Skip to content

fix(artists): Add New Artist opens a shared Spotify search — kills the /artists loop (chat#1867) - #1878

Merged
sweetmantech merged 2 commits into
testfrom
fix/artists-add-artist-spotify-search
Jul 22, 2026
Merged

fix(artists): Add New Artist opens a shared Spotify search — kills the /artists loop (chat#1867)#1878
sweetmantech merged 2 commits into
testfrom
fix/artists-add-artist-spotify-search

Conversation

@sweetmantech

@sweetmantech sweetmantech commented Jul 22, 2026

Copy link
Copy Markdown
Collaborator

The bug

On /artists, Add New Artist → redirects to /?q=create%20a%20new%20artist → shows the onboarding "Finish setting up" UI (not a Spotify search) → its "Open your roster" CTA → back to /artistsinfinite loop.

Root cause: the button (and the sidebar/header equivalents) call toggleCreation (useArtistMode.tsx), which did push("/?q=create a new artist") and relied on the home chat interpreting that query (useCreateArtists.tsx). The onboarding router (#1872) now intercepts the home for incomplete accounts and renders the sequence instead of the chat, so the query never runs. Reproduced end-to-end on test-recoup.vercel.app 2026-07-21 with a fresh account.

The fix

toggleCreation now opens a shared Spotify artist-search dialog instead of redirecting — fixing /artists, the sidebar, and the header at once (they all route through toggleCreation).

  • lib/spotify/parseSpotifyArtistResults.ts — pure parser for the GET /api/spotify/search?type=artist envelope → { id, name, imageUrl, profileUrl, followers }.
  • lib/artists/addSpotifyArtist.ts — create by name (POST /api/artists) then link the Spotify image + profile URL (PATCH /api/artists/{id}), so the roster card gets the real avatar and a Spotify social to enrich.
  • hooks/useSpotifyArtistSearch.ts — debounced, abortable typeahead.
  • hooks/useAddSpotifyArtist.ts — add + refresh/select the roster.
  • components/Artists/SpotifyArtistSearch.tsxthe shared component (avatar · name · followers). Deliberately search-only so it can be reused for the artist-social enrichment slice and the verify-socials Spotify typeahead (both tracked in Onboarding sequence: state-derived resumable flow (valuation aha → roster/socials/catalog verify → first task) + activation fixes #1867).
  • components/Artists/AddArtistDialog.tsx — global dialog, mounted in app/layout.tsx next to ArtistSettingModal.
  • hooks/useArtistMode.tsxtoggleCreation opens the dialog (isCreationOpen / closeCreation); the /?q= redirect is gone.

Tests

TDD red→green — parseSpotifyArtistResults 5/5 (mapping, missing image/followers, URL fallback, malformed input), addSpotifyArtist 2/2 (create→link, image omitted when none). Full tsc: 10 pre-existing errors, 0 new; eslint + prettier clean.

Verification (pending)

Preview verification on the sha-checked deployment: fresh account → /artists → Add New Artist opens the Spotify search (no redirect), pick an artist → lands in the roster with the real image, no loop. Base test per the #1867 branch decision.

Tracking: chat#1867 (🔴 /artists add-artist loop → shared Spotify search).


Summary by cubic

Fixes the infinite loop on /artists when adding a new artist by opening a shared Spotify search dialog instead of redirecting to home. Addresses chat#1867 and applies the same fix to the sidebar and header.

  • Bug Fixes

    • toggleCreation now opens a global dialog (AddArtistDialog) and removes the /?q=create a new artist redirect.
    • Prevents the onboarding router from hijacking the flow and stops the /artists bounce loop (sidebar and header included).
  • Refactors

    • Reused canonical Spotify types from types/spotify and dropped the redundant parseSpotifyArtistResults; useSpotifyArtistSearch now returns data.artists?.items directly. Behavior unchanged.

Written for commit 5b56a8b. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features
    • Added an “Add a new artist” dialog accessible from the artist experience.
    • Added Spotify artist search with typeahead results, images, follower counts, and loading states.
    • Users can select a Spotify artist to add it to their roster, including profile and image details.
    • Added success and error notifications during artist creation.
  • Improvements
    • Artist creation now opens in a shared dialog instead of navigating to a separate route.

… /artists loop)

"Add New Artist" (and the sidebar/header equivalents) called toggleCreation,
which pushed `/?q=create a new artist` and relied on the home CHAT to interpret
it. The onboarding router (chat#1872) now intercepts the home for incomplete
accounts and renders "Finish setting up" instead, so the query never ran — the
"Open your roster" CTA bounced back to /artists → infinite loop.

Replace the redirect with a shared Spotify artist-search dialog:
- lib/spotify/parseSpotifyArtistResults.ts — pure parser for the
  GET /api/spotify/search?type=artist envelope (id/name/imageUrl/profileUrl/followers).
- lib/artists/addSpotifyArtist.ts — create by name (POST /api/artists) then link
  the Spotify image + profile URL (PATCH), so the roster gets real data.
- useSpotifyArtistSearch (debounced, abortable) + useAddSpotifyArtist hooks.
- components/Artists/SpotifyArtistSearch.tsx — the shared typeahead (reused next
  for social enrichment + verify-socials).
- components/Artists/AddArtistDialog.tsx — global dialog, mounted in layout.
- useArtistMode.toggleCreation now opens the dialog (isCreationOpen/closeCreation)
  instead of the redirect — fixes /artists, sidebar, and header at once.

TDD: parseSpotifyArtistResults 5/5, addSpotifyArtist 2/2, red→green. tsc clean on
touched files, eslint/prettier clean.

chat#1867 (/artists add-artist loop).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@cursor

cursor Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@vercel

vercel Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
chat Ready Ready Preview Jul 22, 2026 12:39am

Request Review

@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@sweetmantech, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 29 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7be92fa9-2e27-46eb-bb6a-267cd08d82a6

📥 Commits

Reviewing files that changed from the base of the PR and between 6c02724 and 5b56a8b.

⛔ Files ignored due to path filters (1)
  • lib/artists/__tests__/addSpotifyArtist.test.ts is excluded by !**/*.test.* and included by lib/**
📒 Files selected for processing (5)
  • components/Artists/AddArtistDialog.tsx
  • components/Artists/SpotifyArtistSearch.tsx
  • hooks/useAddSpotifyArtist.ts
  • hooks/useSpotifyArtistSearch.ts
  • lib/artists/addSpotifyArtist.ts
📝 Walkthrough

Walkthrough

Adds a shared artist-creation dialog with debounced Spotify search, normalized results, roster persistence, loading states, and creation state managed locally instead of through query-string navigation.

Changes

Spotify artist creation

Layer / File(s) Summary
Spotify result normalization
lib/spotify/parseSpotifyArtistResults.ts
Defines SpotifyArtistResult and safely converts Spotify payloads into validated artist records with profile, image, and follower data.
Debounced Spotify search UI
hooks/useSpotifyArtistSearch.ts, components/Artists/SpotifyArtistSearch.tsx
Adds debounced, abortable artist searches and renders searchable results, loading states, empty states, avatars, and follower counts.
Roster artist persistence
lib/artists/addSpotifyArtist.ts, hooks/useAddSpotifyArtist.ts
Creates roster artists from Spotify selections, saves Spotify metadata, refreshes artists, and reports operation status through toasts.
Creation dialog integration
hooks/useArtistMode.tsx, components/Artists/AddArtistDialog.tsx, app/layout.tsx
Uses local creation state, connects selection to persistence, closes after successful additions, and mounts the dialog in the root layout.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Possibly related PRs

  • recoupable/chat#1870: Introduces createRosterArtist, which the new Spotify artist persistence helper calls.

Poem

A search box hums a Spotify tune,
Artists appear beneath the moon.
A click saves profiles, bright and new,
The roster grows with a friendly cue.
Dialogs open, then gently close—
Clean little flows from coast to coast.

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Solid & Clean Code ✅ Passed PASS: The PR uses small, focused hooks/components plus pure helper modules; no oversized functions or obvious duplication beyond thin composition.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/artists-add-artist-spotify-search

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6c027244ef

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread hooks/useArtistMode.tsx
if (!email) return;
push("/?q=create a new artist");
clearParams();
setIsCreationOpen(true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reset the legacy creation spinner on the dialog path

When this path is reached from CreateWorkspaceModal.handleCreateArtist, that caller still sets setIsCreatingArtist(true) before calling toggleCreation(), and the only existing reset in useCreateArtists is tied to chat state from the old /?q=create a new artist flow. Since this new dialog path does not create or update that chat, choosing Artist from the New Workspace modal leaves isCreatingArtist stuck true after the dialog opens/closes or after the artist is added, so the sidebar/header artist buttons remain disabled with a spinner until some unrelated chat-state update happens.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
components/Artists/SpotifyArtistSearch.tsx (1)

10-15: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Export the props interface.

SpotifyArtistSearchProps isn't exported, despite being named per convention.

As per coding guidelines, "Export component prop types with explicit ComponentNameProps naming convention."

♻️ Proposed fix
-interface SpotifyArtistSearchProps {
+export interface SpotifyArtistSearchProps {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/Artists/SpotifyArtistSearch.tsx` around lines 10 - 15, Export the
SpotifyArtistSearchProps interface so the component’s prop type is publicly
available, preserving its existing fields and ComponentNameProps naming.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@hooks/useArtistMode.tsx`:
- Around line 18-22: Update toggleCreation in useArtistMode so missing email no
longer produces a silent no-op: surface a small user-facing toast or provide a
disabled/hidden state for the add-artist entry points, while preserving the
existing clearParams and setIsCreationOpen flow for valid accounts.

In `@lib/artists/addSpotifyArtist.ts`:
- Around line 13-30: In lib/artists/addSpotifyArtist.ts lines 13-30, update
addSpotifyArtist to handle saveArtist failures separately after
createRosterArtist succeeds and return the created artist rather than reporting
the entire operation as failed. In hooks/useAddSpotifyArtist.ts lines 22-45,
separate getArtists refresh error handling from artist creation so refresh-only
failures do not trigger the creation-failure toast, and prevent retries from
creating an already-successful Spotify-linked artist.

---

Nitpick comments:
In `@components/Artists/SpotifyArtistSearch.tsx`:
- Around line 10-15: Export the SpotifyArtistSearchProps interface so the
component’s prop type is publicly available, preserving its existing fields and
ComponentNameProps naming.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 405b88b5-c753-4396-8f86-45eaf2d4ac40

📥 Commits

Reviewing files that changed from the base of the PR and between 0391fe0 and 6c02724.

⛔ Files ignored due to path filters (2)
  • lib/artists/__tests__/addSpotifyArtist.test.ts is excluded by !**/*.test.* and included by lib/**
  • lib/spotify/__tests__/parseSpotifyArtistResults.test.ts is excluded by !**/*.test.* and included by lib/**
📒 Files selected for processing (8)
  • app/layout.tsx
  • components/Artists/AddArtistDialog.tsx
  • components/Artists/SpotifyArtistSearch.tsx
  • hooks/useAddSpotifyArtist.ts
  • hooks/useArtistMode.tsx
  • hooks/useSpotifyArtistSearch.ts
  • lib/artists/addSpotifyArtist.ts
  • lib/spotify/parseSpotifyArtistResults.ts

Comment thread hooks/useArtistMode.tsx
Comment on lines 18 to 22
const toggleCreation = () => {
clearParams();
if (!email) return;
push("/?q=create a new artist");
clearParams();
setIsCreationOpen(true);
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether callers of toggleCreation gate/disable the trigger for incomplete accounts
rg -n -B3 -A3 'toggleCreation' --type=tsx --type=ts

Repository: recoupable/chat

Length of output: 181


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== toggleCreation usages ==\n'
rg -n -B3 -A5 'toggleCreation' . --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**'

printf '\n== artist mode hook file ==\n'
git ls-files 'hooks/useArtistMode.tsx' 'hooks/*Artist*' 'components/*Artist*' 'app/*Artist*' 'pages/*Artist*' | sed 's#^`#-` #'

Repository: recoupable/chat

Length of output: 9941


🏁 Script executed:

#!/bin/bash
set -euo pipefail

for f in hooks/useArtistMode.tsx components/Header/Header.tsx components/Artists/Artists.tsx components/SideMenu/SideMenu.tsx components/CreateWorkspaceModal.tsx; do
  echo "===== $f ====="
  wc -l "$f"
  echo "--- outline ---"
  ast-grep outline "$f" --view expanded | sed -n '1,220p'
  echo
done

Repository: recoupable/chat

Length of output: 1027


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== hooks/useArtistMode.tsx around toggleCreation ==\n'
sed -n '1,120p' hooks/useArtistMode.tsx

printf '\n== components/Header/Header.tsx relevant section ==\n'
sed -n '1,140p' components/Header/Header.tsx

printf '\n== components/Artists/Artists.tsx relevant section ==\n'
sed -n '1,120p' components/Artists/Artists.tsx

printf '\n== components/SideMenu/SideMenu.tsx relevant section ==\n'
sed -n '1,140p' components/SideMenu/SideMenu.tsx

printf '\n== components/CreateWorkspaceModal.tsx relevant section ==\n'
sed -n '1,120p' components/CreateWorkspaceModal.tsx

Repository: recoupable/chat

Length of output: 14026


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== email-related gating near add-artist actions ==\n'
rg -n -B4 -A8 'email|toggleCreation|Add New Artist|create a new artist|isIncomplete|incomplete' \
  hooks/useArtistMode.tsx \
  components/Header/Header.tsx \
  components/Artists/Artists.tsx \
  components/SideMenu/SideMenu.tsx \
  components/CreateWorkspaceModal.tsx \
  --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**'

Repository: recoupable/chat

Length of output: 8870


Surface a fallback for incomplete accounts hooks/useArtistMode.tsx:18-22 — this now returns silently when email is missing, but the add-artist entry points still invoke it directly. Without a disabled/hidden state or a small toast, incomplete accounts get a dead click.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@hooks/useArtistMode.tsx` around lines 18 - 22, Update toggleCreation in
useArtistMode so missing email no longer produces a silent no-op: surface a
small user-facing toast or provide a disabled/hidden state for the add-artist
entry points, while preserving the existing clearParams and setIsCreationOpen
flow for valid accounts.

Comment on lines +13 to +30
export async function addSpotifyArtist(
accessToken: string,
artist: SpotifyArtistResult,
orgId?: string | null,
): Promise<ArtistRecord> {
const created = await createRosterArtist(accessToken, artist.name, orgId);

const { artist: updated } = await saveArtist(
accessToken,
created.account_id,
{
...(artist.imageUrl ? { image: artist.imageUrl } : {}),
profileUrls: { SPOTIFY: artist.profileUrl },
},
);

return updated ?? created;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Non-idempotent create+link+refresh chain risks duplicate roster artists on retry. addSpotifyArtist performs createRosterArtist (a non-idempotent write) followed by saveArtist with no compensation if the second call fails, and useAddSpotifyArtist.add wraps that plus the roster getArtists refresh in one try/catch — so any failure after the artist is already created surfaces as a generic "Failed to add artist" toast, and since AddArtistDialog only closes on success, the natural next action is to retry the same artist, calling createRosterArtist again and creating a duplicate.

  • lib/artists/addSpotifyArtist.ts#L13-L30: after createRosterArtist succeeds, catch failures from saveArtist separately and still return the created artist (perhaps flagging that enrichment is incomplete) rather than letting the whole operation appear to fail.
  • hooks/useAddSpotifyArtist.ts#L22-L45: separate the getArtists refresh's error handling from the creation call so a refresh-only failure doesn't produce the same "Failed to add artist" toast as an actual creation failure, and consider guarding against re-creating an artist that already succeeded (e.g., disable re-selecting the same result, or check for an existing Spotify-linked artist before creating).
📍 Affects 2 files
  • lib/artists/addSpotifyArtist.ts#L13-L30 (this comment)
  • hooks/useAddSpotifyArtist.ts#L22-L45
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/artists/addSpotifyArtist.ts` around lines 13 - 30, In
lib/artists/addSpotifyArtist.ts lines 13-30, update addSpotifyArtist to handle
saveArtist failures separately after createRosterArtist succeeds and return the
created artist rather than reporting the entire operation as failed. In
hooks/useAddSpotifyArtist.ts lines 22-45, separate getArtists refresh error
handling from artist creation so refresh-only failures do not trigger the
creation-failure toast, and prevent retries from creating an already-successful
Spotify-linked artist.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

9 issues found across 10 files

Confidence score: 3/5

  • lib/artists/addSpotifyArtist.ts can leave a newly created roster artist persisted when the follow-up PATCH fails, even though the flow reports failure; retries can then create duplicates and inconsistent workspace state. Make the create+patch path atomic or add compensating cleanup of the created artist before merging.
  • hooks/useArtistMode.tsx doesn’t reset the legacy isCreatingArtist flag in the new dialog path, so CreateWorkspaceModal.handleCreateArtist can still trigger the old redirect flow unexpectedly. Reset/clear that flag when switching to toggleCreation() so users stay in the intended flow.
  • components/Artists/SpotifyArtistSearch.tsx and hooks/useSpotifyArtistSearch.ts can show stale or out-of-order results (including after abort), leaving previous query items clickable and risking selection of the wrong artist. Clear/hide results on query change and gate success-state updates on the active, non-aborted request before merging.
  • hooks/useSpotifyArtistSearch.ts + components/Artists/SpotifyArtistSearch.tsx currently treat API/network failures like empty results (res.ok not checked, errors shown as “No artists found”), which can mislead users and hide recoverable failures. Surface explicit error state from the hook and render a retry-friendly message; the ARIA/listbox gaps are lower-risk but should be queued soon for accessibility quality.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="lib/artists/addSpotifyArtist.ts">

<violation number="1" location="lib/artists/addSpotifyArtist.ts:20">
P2: A failed PATCH leaves the newly created roster artist persisted while this function reports failure, so retrying the dialog can create duplicate artists. An atomic create operation or compensating deletion of `created.account_id` on PATCH failure would keep the add flow consistent.</violation>
</file>

<file name="hooks/useSpotifyArtistSearch.ts">

<violation number="1" location="hooks/useSpotifyArtistSearch.ts:20">
P3: The hook is difficult to test and maintain because its request/lifecycle logic is concentrated in a function over twice the repository's 20-line limit. Extracting the fetch/parse callback or lifecycle helper would keep the hook focused.</violation>

<violation number="2" location="hooks/useSpotifyArtistSearch.ts:38">
P2: The Spotify search fetch doesn't check `res.ok` before parsing the response body. A non-2xx response (rate limit, server error, auth failure) gets parsed as JSON, silently falls through `parseSpotifyArtistResults` as "no items", and the UI shows "No artists found." — indistinguishable from a valid empty search. Check `res.ok` and throw or log explicitly so API failures don't masquerade as empty results.</violation>

<violation number="3" location="hooks/useSpotifyArtistSearch.ts:45">
P2: An older search can still publish its parsed results after its controller has been aborted, defeating the hook's out-of-order protection and showing results for the wrong query. Guard the success update with `!controller.signal.aborted` like the catch and finally paths.</violation>
</file>

<file name="components/Artists/SpotifyArtistSearch.tsx">

<violation number="1" location="components/Artists/SpotifyArtistSearch.tsx:36">
P3: The search input acts as a combobox but lacks the ARIA combobox pattern. Screen reader users won't know that typing yields a selectable list of Spotify artists below the input. Add `role="combobox"`, `aria-expanded` (reflecting whether the listbox is visible), and `aria-controls` pointing to the results container's `id` to align with the WAI-ARIA combobox pattern and improve discoverability for assistive technology users.</violation>

<violation number="2" location="components/Artists/SpotifyArtistSearch.tsx:49">
P2: This announces a listbox to assistive technology without implementing listbox keyboard/selection behavior, while also placing loading and empty messages inside the option container. Either implement the listbox interaction model or use an ordinary results container with keyboard-accessible buttons and status messages outside it.</violation>

<violation number="3" location="components/Artists/SpotifyArtistSearch.tsx:59">
P2: Network or Spotify API failures are presented as “No artists found,” which misleads users and gives them no recovery path. Exposing a search error from `useSpotifyArtistSearch` would allow this state to offer an error/retry message instead.</violation>

<violation number="4" location="components/Artists/SpotifyArtistSearch.tsx:62">
P2: Typing a new search can leave results for the previous query displayed and clickable until the new request finishes. Clearing results when a query changes, or hiding them while `isSearching`, would prevent adding an artist that does not match the current input.</violation>
</file>

<file name="hooks/useArtistMode.tsx">

<violation number="1" location="hooks/useArtistMode.tsx:21">
P2: The new dialog path doesn't reset the legacy `isCreatingArtist` flag. When `CreateWorkspaceModal.handleCreateArtist` calls `setIsCreatingArtist(true)` before invoking `toggleCreation()`, the old redirect flow would eventually clear it via chat-state side-effects. Since the dialog path bypasses that chat flow entirely, `isCreatingArtist` stays `true` after the dialog opens/closes, leaving the sidebar/header artist buttons disabled with a spinner until an unrelated state update happens. `toggleCreation` (or `closeCreation`) should reset that flag.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

): Promise<ArtistRecord> {
const created = await createRosterArtist(accessToken, artist.name, orgId);

const { artist: updated } = await saveArtist(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: A failed PATCH leaves the newly created roster artist persisted while this function reports failure, so retrying the dialog can create duplicate artists. An atomic create operation or compensating deletion of created.account_id on PATCH failure would keep the add flow consistent.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At lib/artists/addSpotifyArtist.ts, line 20:

<comment>A failed PATCH leaves the newly created roster artist persisted while this function reports failure, so retrying the dialog can create duplicate artists. An atomic create operation or compensating deletion of `created.account_id` on PATCH failure would keep the add flow consistent.</comment>

<file context>
@@ -0,0 +1,30 @@
+): Promise<ArtistRecord> {
+  const created = await createRosterArtist(accessToken, artist.name, orgId);
+
+  const { artist: updated } = await saveArtist(
+    accessToken,
+    created.account_id,
</file context>

Comment thread hooks/useSpotifyArtistSearch.ts Outdated
{ signal: controller.signal },
);
const json = await res.json();
setResults(parseSpotifyArtistResults(json));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: An older search can still publish its parsed results after its controller has been aborted, defeating the hook's out-of-order protection and showing results for the wrong query. Guard the success update with !controller.signal.aborted like the catch and finally paths.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At hooks/useSpotifyArtistSearch.ts, line 45:

<comment>An older search can still publish its parsed results after its controller has been aborted, defeating the hook's out-of-order protection and showing results for the wrong query. Guard the success update with `!controller.signal.aborted` like the catch and finally paths.</comment>

<file context>
@@ -0,0 +1,60 @@
+          { signal: controller.signal },
+        );
+        const json = await res.json();
+        setResults(parseSpotifyArtistResults(json));
+      } catch {
+        if (!controller.signal.aborted) setResults([]);
</file context>
Suggested change
setResults(parseSpotifyArtistResults(json));
if (!controller.signal.aborted) {
setResults(parseSpotifyArtistResults(json));
}


<div
className="flex max-h-72 flex-col gap-1 overflow-y-auto"
role="listbox"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: This announces a listbox to assistive technology without implementing listbox keyboard/selection behavior, while also placing loading and empty messages inside the option container. Either implement the listbox interaction model or use an ordinary results container with keyboard-accessible buttons and status messages outside it.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At components/Artists/SpotifyArtistSearch.tsx, line 49:

<comment>This announces a listbox to assistive technology without implementing listbox keyboard/selection behavior, while also placing loading and empty messages inside the option container. Either implement the listbox interaction model or use an ordinary results container with keyboard-accessible buttons and status messages outside it.</comment>

<file context>
@@ -0,0 +1,99 @@
+
+      <div
+        className="flex max-h-72 flex-col gap-1 overflow-y-auto"
+        role="listbox"
+        aria-label="Spotify artist results"
+      >
</file context>

)}
{!isSearching && hasQuery && results.length === 0 && (
<p className="px-1 py-2 text-sm text-muted-foreground">
No artists found.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Network or Spotify API failures are presented as “No artists found,” which misleads users and gives them no recovery path. Exposing a search error from useSpotifyArtistSearch would allow this state to offer an error/retry message instead.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At components/Artists/SpotifyArtistSearch.tsx, line 59:

<comment>Network or Spotify API failures are presented as “No artists found,” which misleads users and gives them no recovery path. Exposing a search error from `useSpotifyArtistSearch` would allow this state to offer an error/retry message instead.</comment>

<file context>
@@ -0,0 +1,99 @@
+        )}
+        {!isSearching && hasQuery && results.length === 0 && (
+          <p className="px-1 py-2 text-sm text-muted-foreground">
+            No artists found.
+          </p>
+        )}
</file context>

No artists found.
</p>
)}
{results.map((artist) => (

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Typing a new search can leave results for the previous query displayed and clickable until the new request finishes. Clearing results when a query changes, or hiding them while isSearching, would prevent adding an artist that does not match the current input.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At components/Artists/SpotifyArtistSearch.tsx, line 62:

<comment>Typing a new search can leave results for the previous query displayed and clickable until the new request finishes. Clearing results when a query changes, or hiding them while `isSearching`, would prevent adding an artist that does not match the current input.</comment>

<file context>
@@ -0,0 +1,99 @@
+            No artists found.
+          </p>
+        )}
+        {results.map((artist) => (
+          <button
+            key={artist.id}
</file context>

const controller = new AbortController();
const timer = setTimeout(async () => {
try {
const res = await fetch(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The Spotify search fetch doesn't check res.ok before parsing the response body. A non-2xx response (rate limit, server error, auth failure) gets parsed as JSON, silently falls through parseSpotifyArtistResults as "no items", and the UI shows "No artists found." — indistinguishable from a valid empty search. Check res.ok and throw or log explicitly so API failures don't masquerade as empty results.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At hooks/useSpotifyArtistSearch.ts, line 38:

<comment>The Spotify search fetch doesn't check `res.ok` before parsing the response body. A non-2xx response (rate limit, server error, auth failure) gets parsed as JSON, silently falls through `parseSpotifyArtistResults` as "no items", and the UI shows "No artists found." — indistinguishable from a valid empty search. Check `res.ok` and throw or log explicitly so API failures don't masquerade as empty results.</comment>

<file context>
@@ -0,0 +1,60 @@
+    const controller = new AbortController();
+    const timer = setTimeout(async () => {
+      try {
+        const res = await fetch(
+          `${getClientApiBaseUrl()}/api/spotify/search?q=${encodeURIComponent(
+            trimmed,
</file context>

Comment thread hooks/useArtistMode.tsx
if (!email) return;
push("/?q=create a new artist");
clearParams();
setIsCreationOpen(true);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The new dialog path doesn't reset the legacy isCreatingArtist flag. When CreateWorkspaceModal.handleCreateArtist calls setIsCreatingArtist(true) before invoking toggleCreation(), the old redirect flow would eventually clear it via chat-state side-effects. Since the dialog path bypasses that chat flow entirely, isCreatingArtist stays true after the dialog opens/closes, leaving the sidebar/header artist buttons disabled with a spinner until an unrelated state update happens. toggleCreation (or closeCreation) should reset that flag.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At hooks/useArtistMode.tsx, line 21:

<comment>The new dialog path doesn't reset the legacy `isCreatingArtist` flag. When `CreateWorkspaceModal.handleCreateArtist` calls `setIsCreatingArtist(true)` before invoking `toggleCreation()`, the old redirect flow would eventually clear it via chat-state side-effects. Since the dialog path bypasses that chat flow entirely, `isCreatingArtist` stays `true` after the dialog opens/closes, leaving the sidebar/header artist buttons disabled with a spinner until an unrelated state update happens. `toggleCreation` (or `closeCreation`) should reset that flag.</comment>

<file context>
@@ -1,24 +1,28 @@
     if (!email) return;
-    push("/?q=create a new artist");
+    clearParams();
+    setIsCreationOpen(true);
   };
 
</file context>

* `GET /api/spotify/search?type=artist` endpoint. Aborts the in-flight
* request when the query changes so results never arrive out of order.
*/
export function useSpotifyArtistSearch(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The hook is difficult to test and maintain because its request/lifecycle logic is concentrated in a function over twice the repository's 20-line limit. Extracting the fetch/parse callback or lifecycle helper would keep the hook focused.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At hooks/useSpotifyArtistSearch.ts, line 20:

<comment>The hook is difficult to test and maintain because its request/lifecycle logic is concentrated in a function over twice the repository's 20-line limit. Extracting the fetch/parse callback or lifecycle helper would keep the hook focused.</comment>

<file context>
@@ -0,0 +1,60 @@
+ * `GET /api/spotify/search?type=artist` endpoint. Aborts the in-flight
+ * request when the query changes so results never arrive out of order.
+ */
+export function useSpotifyArtistSearch(
+  query: string,
+): UseSpotifyArtistSearchResult {
</file context>

<div className="flex flex-col gap-3">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
<Input

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The search input acts as a combobox but lacks the ARIA combobox pattern. Screen reader users won't know that typing yields a selectable list of Spotify artists below the input. Add role="combobox", aria-expanded (reflecting whether the listbox is visible), and aria-controls pointing to the results container's id to align with the WAI-ARIA combobox pattern and improve discoverability for assistive technology users.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At components/Artists/SpotifyArtistSearch.tsx, line 36:

<comment>The search input acts as a combobox but lacks the ARIA combobox pattern. Screen reader users won't know that typing yields a selectable list of Spotify artists below the input. Add `role="combobox"`, `aria-expanded` (reflecting whether the listbox is visible), and `aria-controls` pointing to the results container's `id` to align with the WAI-ARIA combobox pattern and improve discoverability for assistive technology users.</comment>

<file context>
@@ -0,0 +1,99 @@
+    <div className="flex flex-col gap-3">
+      <div className="relative">
+        <Search className="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
+        <Input
+          autoFocus={autoFocus}
+          value={query}
</file context>

@sweetmantech

Copy link
Copy Markdown
Collaborator Author

Preview verification — fix confirmed

Verified on the sha-checked preview https://chat-8esoouvr3-recoup.vercel.app (head 6c027244) with a brand-new account (sweetmantech+july2120261846@, empty roster).

Step Before (bug) After (this PR)
/artistsAdd New Artist redirect to /?q=create%20a%20new%20artist → "Finish setting up" opens the Spotify search dialog — URL stays /artists
Search "Del Water Gap" 20 real Spotify results (avatar · name · followers) ✅
Pick an artist toast "added to your roster", card appears with the real Spotify avatar (i.scdn.co/…756c9f2be6294d04107ae215), dialog closes, no loop

DB after add (artist 0a8bebf3): a Spotify social linked with the correct profile_url (…/artist/0xPoVNPnxIIUS1vrxAYV00), and the artist's display image set to the real Spotify photo. The social row's avatar/followerCount/username remain unenriched — that's the separate enrichment slice (social-scrape) that reuses this same SpotifyArtistSearch component.

The SideMenu and Header "add artist" entry points route through the same toggleCreation, so they're fixed by construction too.

Screenshots

Spotify search dialog (Add New Artist — no redirect):

Spotify artist search dialog

Roster after adding (real Spotify avatar, no loop):

Roster with added artist

Tests: parseSpotifyArtistResults 5/5, addSpotifyArtist 2/2 (red→green); tsc 0 new; eslint/prettier clean.

…er (−106 LOC)

Simplification pass on the add-artist slice:
- Delete lib/spotify/parseSpotifyArtistResults.ts (+ its 5 tests) and the custom
  SpotifyArtistResult type — types/spotify.ts already exports SpotifySearchResponse
  ({ artists?: { items: SpotifyArtistSearchResult[] } }) and SpotifyArtistSearchResult
  (id/name/external_urls/images/followers). useSpotifyArtistSearch now reads
  `data.artists?.items` directly and returns the canonical type; the component and
  addSpotifyArtist read images[0].url / followers.total / external_urls.spotify inline.

Kept (checked, "no simpler"): addSpotifyArtist's two-step create→link, because
POST /api/artists only accepts a name; and useSpotifyArtistSearch, since there is
no existing debounced-search hook to reuse.

Net -106 LOC; behavior unchanged; addSpotifyArtist 2/2 green, tsc 0 new.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@sweetmantech

Copy link
Copy Markdown
Collaborator Author

Simplification pass (5b56a8b7, −106 LOC)

Audited for duplication/removable code:

  • Removed lib/spotify/parseSpotifyArtistResults.ts + its 5 tests + the custom SpotifyArtistResult type. types/spotify.ts already exports SpotifySearchResponse ({ artists?: { items: SpotifyArtistSearchResult[] } }) and SpotifyArtistSearchResult — so the hook now reads data.artists?.items directly and the component/addSpotifyArtist read images[0].url / followers.total / external_urls.spotify inline. That parser was re-inventing already-typed data.

Kept, deliberately (the "no simpler" line):

  • addSpotifyArtist's two-step create→link — POST /api/artists only accepts a name, so the Spotify profile/image must be linked via the follow-up PATCH.
  • useSpotifyArtistSearch (debounced/abortable) — there's no existing search-debounce hook to reuse.
  • SpotifyArtistSearch dropdown — the existing SpotifyContentCard is a horizontal display for tool results, not a search-select input.

Behavior is unchanged (pure type/parser refactor); addSpotifyArtist 2/2 green, tsc 0 new, eslint/prettier clean. The earlier end-to-end preview verification still holds.

@sweetmantech

Copy link
Copy Markdown
Collaborator Author

Re-verified on the simplified build (5b56a8b7)

Preview https://chat-aom2e51x8-recoup.vercel.app, existing test account — added a different artist to confirm the −106 LOC refactor is behavior-identical:

  • /artistsAdd New Artist → Spotify search dialog opens, URL stays /artists (no redirect/loop) ✅
  • Searched "Phoebe Bridgers" → real results (top: 3.4M followers) ✅
  • Selected → toast "Phoebe Bridgers added to your roster", card appears with the real Spotify avatar
  • DB: new artist linked to the correct Spotify profile (…/artist/1r1uxoy19fzMxunt3ONAkG); all roster artists spotify_linked = true
  • Console clean (0 errors)

Behavior matches the pre-refactor verification exactly. Ready for review.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 8 files (changes from recent commits).

Requires human review: Auto-approval blocked by 9 unresolved issues from previous reviews.

Re-trigger cubic

@sweetmantech
sweetmantech merged commit fd463f8 into test Jul 22, 2026
3 of 4 checks passed
sweetmantech added a commit that referenced this pull request Jul 22, 2026
… artist add (chat#1867) (#1879)

* feat(onboarding): seed the catalog by kicking /api/valuation on first artist add

Closes the onboarding catalog dead-end for direct signups. When the first artist
is added via the Spotify search (#1878 already links their Spotify profile),
useAddSpotifyArtist now fire-and-forget kicks POST /api/valuation
{ spotify_artist_id } (api#776) — only when the account has no catalog yet —
which materializes the catalog + value band in ~20s. So the "Claim your catalog"
step is already complete by the time the user reaches it, and they flow through
to the first-task step (weekly report emails).

- lib/valuation/runValuation.ts — client POST /api/valuation (TDD 2/2).
- hooks/useAddSpotifyArtist.ts — background kick on first add (gated on an empty,
  loaded catalogs query), surfaced via a toast.promise ("Valuing … → Your catalog
  is ready"), invalidating the catalogs query on success so the sequence advances.

chat#1867 (seed onboarding catalog on first artist add).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(models): fix organizeModels fixture — featured id gpt-5.2 → gpt-5.5 (post-#1877)

The featured-models refresh in #1877 replaced openai/gpt-5.2 with openai/gpt-5.5,
but organizeModels.test.ts still asserted gpt-5.2 was featured — failing CI on
test. Update the fixture + assertion to a currently-featured id.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sweetmantech added a commit that referenced this pull request Jul 22, 2026
…t-task emails, add-artist, Kimi K3 (#1880)

* feat(onboarding): state-derived onboarding router with resume-on-landing + skip-for-now (#1872)

* feat(onboarding): state-derived onboarding router with resume-on-landing + skip-for-now

On every authenticated landing, chat home derives the onboarding step
from the activation checkpoint predicates (has artists -> artists have
socials -> has claimed catalog -> has enabled task) over existing data
sources, never a stored wizard cursor (#1867). Incomplete
accounts resume the sequence at the first unmet step; an always-visible
skip drops to the app with a dismissible session-scoped checklist;
activated accounts never see it. Step cards are titled placeholders
linking to the existing pages until the step PRs land.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(onboarding): review fixes — checklist clipped by right rail, account-scoped session flags, storage safety, fresh catalogs per landing, roster-error fail-open, h3 card heading

Fixes the preview-verified dismiss bug and all cubic findings on #1872:

- Dismiss live bug: the checklist was position:fixed to the viewport, so
  the z-[65] ArtistsSidebar rail overlapped its right edge and clipped
  the dismiss button off-screen. Now absolute within the (relative) home
  content area. Gate state was already single-owner (HomePage passes
  callbacks down) — locked in with a HomePage-level dismiss/skip/resume
  test.
- P1 cross-account leak: skip/dismiss sessionStorage keys are scoped by
  account id and re-derived when the account in the tab changes
  (useOnboardingSessionFlags).
- P2 storage safety: read/write go through safe helpers that no-op on
  throwing storage so the gate fails open instead of crashing.
- P2 catalogs staleness: useRefreshCatalogsOnLanding invalidates the
  catalogs cache on onboarding mount for one fresh read per landing;
  useCatalogs behavior unchanged for other consumers.
- P2 roster-error fail-open: useArtistsRoster now exposes isError;
  deriveOnboardingState keeps isReady false on roster error so the view
  resolves to none.
- P3 hook length: projection extracted to pure deriveOnboardingState.
- P3 heading hierarchy: step card h1 -> h3 under the container h2.

Tests: TDD red-first; +26 tests (jsdom + @testing-library/react added as
devDeps; vitest include extended to .test.tsx). 174 passing; tsc clean
apart from the 7 pre-existing main errors in lib/emails tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(onboarding): make the skip checklist a persistent, non-dismissible reminder

Remove the checklist-dismissed escape hatch entirely. After "skip for now"
the bottom-right "Finish setting up" card is now always present while
onboarding is incomplete (survives refresh via the session skip flag), and
clicking it re-opens the sequence — there is no way to permanently hide it.

Net-removal: drops the second session flag, its storage read/write usage,
the dismissChecklist callback threading, the X/Dismiss button, and the
separate "Resume setup" button (the card header is now the resume trigger).
getOnboardingView collapses to two views (sequence | checklist) and
OnboardingFlag to a single member.

+47 / -134. Full suite 181 pass, tsc clean on touched files, prettier clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(onboarding): restore plain title + add explicit "Continue" button

The checklist header "Finish setting up" is a plain title again (it read as
non-clickable). Re-opening the sequence is now a clearly-styled primary
"Continue" button at the bottom of the card, replacing the ambiguous
click-the-header affordance.

Suite 181 pass, tsc clean on touched files, prettier clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* feat: post-valuation catalog report on /catalogs/[catalogId] (chat#1867 sequence step 1) (#1873)

The marketing valuation CTA landed on a bare Catalog Songs admin screen.
/catalogs/{id} now opens as a real catalog report: valuation echo
(central value + range via the ported marketing formula), lifetime
streams, tracks/releases measured, per-release table with album art,
per-section diagnosis + prescription copy, and one primary CTA
(set up your weekly report -> /tasks). The existing songs/ISRC
management UI stays reachable as a secondary Manage songs tab.

Valuation math mirrors marketing/lib/valuation (identical constants;
age from the api's catalog_age_years, 5y default). Release rollups are
null-safe on album/name/artist metadata independently of the sibling
crash-fix PR. Reuses the chat#1852 useCatalogMeasurements hook,
extending its response type with the v2 aggregate fields as optionals.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* feat: roster + socials verification onboarding steps (chat#1867) (#1870)

* feat: roster + socials verification onboarding steps (chat#1867)

Two self-contained onboarding step components for the chat#1867
sequence, mounted standalone at /onboarding/roster so the slice is
user-testable before the onboarding router lands:

- ConfirmRosterStep: shows the auto-created artist(s) from the
  valuation flow, inline "add another artist" for multi-artist
  managers (existing POST /api/artists endpoint), confirm to advance.
- VerifySocialsStep: per rostered artist, lists the auto-matched
  socials with handle + follower count; each match is confirmed or
  rejected, wrong matches are fixed by pasting the correct profile
  link (existing PATCH /api/artists/{id} profileUrls path), and
  artists with no socials record an explicit "none".

Verification state is pure client-side logic (lib/onboarding/*,
TDD'd): verdict reducers, per-artist and step-level resolution
predicates, PATCH payload building with APPPLE->APPLE platform-key
normalization, and a follower-count accessor tolerant of the API's
snake_case rows behind chat's camelCase SOCIAL type.

No new endpoints, tables, or context providers - hooks compose the
existing ArtistProvider / OrganizationProvider / Privy auth.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* refactor(onboarding): simplify verify-socials to accept-by-default (chat#1867)

Per design review: drop the per-social Correct/Wrong verdicts, the
resolution/gating machinery, and the explicit "This artist has no socials"
button. Matches are accepted by default; the only actions are Edit (fix a
wrong link, existing PATCH profileUrls path) and — for an artist with no
matches — a soft nudge + "Add a profile". Continue always proceeds (empty
= implicit none).

Deletes the verdict code that would otherwise be merged and immediately
removed: socialVerificationTypes, applySocialVerdict, isArtistSocialsResolved,
areAllArtistsResolved, markArtistHasNoSocials, findSocialIdByPlatform,
useSocialsVerification, SocialVerifyRow (+ their tests). Net −13 files.

Note: "Remove/delete a social" (the other half of the design) needs a new
api endpoint — the api's PATCH profileUrls is per-platform merge with no
delete-social path — tracked as a fast-follow on chat#1867.

Full suite 144 pass, tsc clean on touched files, lint + prettier clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(onboarding): always offer Add-a-profile per artist, not just empty ones

A matched-social list can still be missing platforms (e.g. Spotify found
but no Instagram). Surface 'Add a profile' below every artist's socials,
not only when zero were auto-matched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* feat(onboarding): first-task step — pre-run weekly report + confirm schedule (#1871)

Sequence final step for chat#1867 (respec 2026-07-20, pre-run shape):
generate the first weekly catalog report immediately through the normal
chat pipeline (same POST /api/chat transport a send uses), show it
finished, then ask one question — "get this every Monday?". Confirm
creates the enabled weekly task via the existing POST /api/tasks path
(which also mints the schedule) and shows the next-run time; decline
creates nothing.

Mounted standalone at /onboarding/first-task so the step is
user-testable before the OnboardingSequence container exists. The
pre-run prompt and the scheduled task prompt come from one builder, so
the preview the user confirms is byte-for-byte what Monday's run uses.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>

* feat(onboarding): first-task weekly report actually emails — LA EQUIS template + Kimi K3 (chat#1867) (#1877)

* feat(onboarding): first-task weekly report actually emails (LA EQUIS template + Kimi K3)

The first-task prompt generated a report but never emailed it (email is a
prompt-driven send_email call, and the prompt had no recipient or send
instruction) — proven by firing the created task: the agent ran and persisted
a report, but email_send_log stayed empty.

Generalize buildFirstTaskPrompt from the proven LA EQUIS weekly report
(scheduled_action 39fb5f68, running week over week): resolve the artist's
Spotify id from its connected profile -> capture this week's play counts ->
compute real week-over-week per-track deltas -> build a fully hex-specced
inline-CSS HTML email -> send via the recoup-platform-email-helper skill ->
confirm the Resend id. Carries the sandbox no-python and anti-fabrication
guardrails verbatim (what makes the send reliable). Thread the account email
(recipientEmail) + artistAccountId through both callers so the pre-run preview
and the Monday scheduled task both send to the account holder.

Bump both surfaces from DEFAULT_MODEL (openai/gpt-5.4-mini — completes the data
calls but gives up before composing the long HTML email, the LA EQUIS lesson)
to a new REPORT_MODEL (moonshotai/kimi-k3), which reliably composes and sends.

chat#1867 (first-task email-prompt gap).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(onboarding): default model -> Kimi K3 (KISS); harden email-less + shell paths

Address review feedback on #1877:

- Sweets (KISS): collapse the new REPORT_MODEL constant into DEFAULT_MODEL —
  set DEFAULT_MODEL = moonshotai/kimi-k3 app-wide (the fast gpt-5.4-mini default
  gave up before composing the long HTML report email). Revert both onboarding
  hooks to DEFAULT_MODEL; relabel the featured picker entry (id: DEFAULT_MODEL)
  from "GPT-5.4 Mini" to "Kimi K3" so it isn't mislabeled.
- CodeRabbit (email-less login): a wallet/phone/social-only Privy account has no
  email — FirstTaskStep now shows a distinct "add an email" alert (not an
  infinite "Preparing…"), and useConfirmFirstTask's onError distinguishes
  EMAIL_REQUIRED from a generic retry.
- Codex (shell-safety): artist/track/album names are user-entered; add a prompt
  guardrail to pass them as single quoted args so a name with quotes/;/$() can't
  alter the email-helper command.

Codex "non-Pro users get rejected" P2 is a false positive: the prior default
(gpt-5.4-mini) is also non-free by isFreeModel yet ships as the default, so the
API does not hard-block non-free default models.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* Update lib/consts.ts

* chore(models): refresh featured model list to latest per series

Sweets: while touching the featured list, bring every entry to its series'
latest (verified present in the AI Gateway catalog):
- GPT-5.2 -> GPT-5.5
- Claude Opus 4.5 -> 4.8
- Claude Sonnet 4.5 -> Sonnet 5
- Gemini 2.5 Flash Lite -> Gemini 3.5 Flash Lite
- Gemini 3 Pro -> Gemini 3.1 Pro
- Grok 4 -> Grok 4.5
(Kimi K3 already latest.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(chat): reuse the real Message renderer in the first-task pre-run (DRY)

The onboarding pre-run rendered only the last assistant message's TEXT
(getReportTextFromMessages -> <Response>), dropping tool-call and reasoning
components — a downgraded, diverging copy of the chat UI. #1877's tool-heavy
email workflow made the gap obvious (raw narration, no tool components).

Reuse the normal chat's <Message>/<MessageParts> so tool calls/results get their
real components. MessageParts was coupled to useVercelChatContext (status +
reload); decouple it: both are now optional props with a context fallback, so
the normal chat is behaviorally unchanged (props omitted -> context used) while
the pre-run supplies them without mounting a provider (VercelChatProvider owns
its own useChat instance, so wrapping the pre-run would double the chat).

- VercelChatProvider: export VercelChatContext for the optional read.
- MessageParts/Message: optional status/reload props, fall back to context.
- useFirstTaskReport: expose messages + status.
- FirstTaskReportRun: render assistant messages via <Message> (filtering out the
  auto-sent prompt user message), not a text dump.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(artists): Add New Artist opens a shared Spotify search — kills the /artists loop (chat#1867) (#1878)

* fix(artists): Add New Artist opens a shared Spotify search (kills the /artists loop)

"Add New Artist" (and the sidebar/header equivalents) called toggleCreation,
which pushed `/?q=create a new artist` and relied on the home CHAT to interpret
it. The onboarding router (chat#1872) now intercepts the home for incomplete
accounts and renders "Finish setting up" instead, so the query never ran — the
"Open your roster" CTA bounced back to /artists → infinite loop.

Replace the redirect with a shared Spotify artist-search dialog:
- lib/spotify/parseSpotifyArtistResults.ts — pure parser for the
  GET /api/spotify/search?type=artist envelope (id/name/imageUrl/profileUrl/followers).
- lib/artists/addSpotifyArtist.ts — create by name (POST /api/artists) then link
  the Spotify image + profile URL (PATCH), so the roster gets real data.
- useSpotifyArtistSearch (debounced, abortable) + useAddSpotifyArtist hooks.
- components/Artists/SpotifyArtistSearch.tsx — the shared typeahead (reused next
  for social enrichment + verify-socials).
- components/Artists/AddArtistDialog.tsx — global dialog, mounted in layout.
- useArtistMode.toggleCreation now opens the dialog (isCreationOpen/closeCreation)
  instead of the redirect — fixes /artists, sidebar, and header at once.

TDD: parseSpotifyArtistResults 5/5, addSpotifyArtist 2/2, red→green. tsc clean on
touched files, eslint/prettier clean.

chat#1867 (/artists add-artist loop).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(artists): reuse canonical Spotify types; drop redundant parser (−106 LOC)

Simplification pass on the add-artist slice:
- Delete lib/spotify/parseSpotifyArtistResults.ts (+ its 5 tests) and the custom
  SpotifyArtistResult type — types/spotify.ts already exports SpotifySearchResponse
  ({ artists?: { items: SpotifyArtistSearchResult[] } }) and SpotifyArtistSearchResult
  (id/name/external_urls/images/followers). useSpotifyArtistSearch now reads
  `data.artists?.items` directly and returns the canonical type; the component and
  addSpotifyArtist read images[0].url / followers.total / external_urls.spotify inline.

Kept (checked, "no simpler"): addSpotifyArtist's two-step create→link, because
POST /api/artists only accepts a name; and useSpotifyArtistSearch, since there is
no existing debounced-search hook to reuse.

Net -106 LOC; behavior unchanged; addSpotifyArtist 2/2 green, tsc 0 new.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(onboarding): seed the catalog by kicking /api/valuation on first artist add (chat#1867) (#1879)

* feat(onboarding): seed the catalog by kicking /api/valuation on first artist add

Closes the onboarding catalog dead-end for direct signups. When the first artist
is added via the Spotify search (#1878 already links their Spotify profile),
useAddSpotifyArtist now fire-and-forget kicks POST /api/valuation
{ spotify_artist_id } (api#776) — only when the account has no catalog yet —
which materializes the catalog + value band in ~20s. So the "Claim your catalog"
step is already complete by the time the user reaches it, and they flow through
to the first-task step (weekly report emails).

- lib/valuation/runValuation.ts — client POST /api/valuation (TDD 2/2).
- hooks/useAddSpotifyArtist.ts — background kick on first add (gated on an empty,
  loaded catalogs query), surfaced via a toast.promise ("Valuing … → Your catalog
  is ready"), invalidating the catalogs query on success so the sequence advances.

chat#1867 (seed onboarding catalog on first artist add).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(models): fix organizeModels fixture — featured id gpt-5.2 → gpt-5.5 (post-#1877)

The featured-models refresh in #1877 replaced openai/gpt-5.2 with openai/gpt-5.5,
but organizeModels.test.ts still asserted gpt-5.2 was featured — failing CI on
test. Update the fixture + assertion to a currently-featured id.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
sweetmantech added a commit that referenced this pull request Jul 22, 2026
Reuse the #1878 SpotifyArtistSearch typeahead in the verify-socials
Add-a-profile and Edit flows instead of paste-a-link. Picking an artist
submits their Spotify profile URL (keyed as SPOTIFY by the existing
platform-detection path). Paste-a-link stays as a fallback for the
non-Spotify platforms the search can't reach.

Item 3b of #1881.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant