Skip to content

Enhance chat functionality by integrating model ID persistence and updating chat update parameters. #1779

Closed
ahmednahima0-beep wants to merge 7 commits into
testfrom
fix/persist-chat-model-id
Closed

Enhance chat functionality by integrating model ID persistence and updating chat update parameters. #1779
ahmednahima0-beep wants to merge 7 commits into
testfrom
fix/persist-chat-model-id

Conversation

@ahmednahima0-beep

@ahmednahima0-beep ahmednahima0-beep commented Jun 4, 2026

Copy link
Copy Markdown
Collaborator

Added usePersistChatModelId to useVercelChat for session and chat ID management. Updated updateChat to accept an optional modelId and modified the request body construction to include it.


Summary by cubic

Persist and hydrate the selected model per chat, and support updating model and title via PATCH. Hydration now requires auth and avoids overwriting on failure, keeping the picker and chats.model_id aligned for accurate billing.

  • New Features

    • Added useChatModelSync to hydrate via getSessionChat and persist picker changes to chats.model_id, serializing PATCH so the latest selection wins.
    • Extended updateChat to accept optional modelId and title, building the body with buildPatchChatBody (throws if neither). Added tests.
  • Refactors

    • Split logic into useHydrateChatModelFromDb and usePersistChatModelToDb; removed usePersistChatModelId.
    • Integrated into useVercelChat and gated hydration, message load, and first-send by routeChatId matching transportChatId.
    • Hydration now requires an access token and uses the picker’s initial value on failure to unlock manual changes without auto-PATCH; improved logging and state handling.

Written for commit 3acaf5e. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • New Features

    • Chat model selection now syncs with and persists to your account and is automatically restored when reopening conversations.
  • Improvements

    • Model and chat metadata updates (including model choice and title) are now saved more reliably.
    • Conversation loading and new-chat behavior are more consistent with navigation, reducing duplicate or missing messages.
    • Improved error handling when fetching chat data for a smoother experience.

@vercel

vercel Bot commented Jun 4, 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 Jun 4, 2026 9:50am

Request Review

@coderabbitai

coderabbitai Bot commented Jun 4, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@ahmednahima0-beep, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 43 minutes and 59 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

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.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2d80279c-a967-469b-9316-b36113a172e3

📥 Commits

Reviewing files that changed from the base of the PR and between a7f81df and 3acaf5e.

📒 Files selected for processing (1)
  • hooks/useHydrateChatModelFromDb.ts
📝 Walkthrough

Walkthrough

Adds bidirectional model synchronization: a session-scoped GET for chat metadata, a patch-body builder and updateChat extension to accept modelId, hydration and persistence hooks that serialize/deduplicate writes, an orchestration hook, and integration into useVercelChat using routeChatId gating.

Changes

Chat model persistence and synchronization

Layer / File(s) Summary
Session-scoped chat fetch API
lib/chats/getSessionChat.ts
getSessionChat fetches session-scoped chat metadata with Authorization header, no-store caching, structured error parsing, and Zod validation.
Patch request builder contract and implementation
lib/chats/buildPatchChatBody.ts
Adds PatchChatBody (title?, modelId?) and buildPatchChatBody that includes only defined fields and errors when none are provided.
updateChat API extension for modelId
lib/chats/updateChat.ts
UpdateChatParams.title becomes optional and modelId? is added; updateChat uses buildPatchChatBody({ title, modelId }) for PATCH bodies.
Model hydration from database
hooks/useHydrateChatModelFromDb.ts
useHydrateChatModelFromDb conditionally fetches persisted model via getSessionChat, updates setModel/lastSyncedRef when present, marks hydrated, and supports cancellation.
Model persistence to database
hooks/usePersistChatModelToDb.ts
usePersistChatModelToDb deduplicates writes with lastSyncedRef, serializes persistence via persistChainRef, fetches access token, and PATCHes modelId via updateChat with error handling.
Model sync orchestration hook
hooks/useChatModelSync.ts
useChatModelSync manages hydrated, lastSyncedRef, persistChainRef, and wires hydration and persistence hooks; resets on session/chat/hydration flag changes.
Integration into useVercelChat router
hooks/useVercelChat.ts
Adds useChatModelSync, derives routeChatId from params, gates hydration/message loading on routeChatId === transportChatId, and switches new-chat optimistic path to use routeChatId.

Sequence Diagram

sequenceDiagram
  participant useVercelChat
  participant useChatModelSync
  participant useHydrateChatModelFromDb
  participant getSessionChat
  participant usePersistChatModelToDb
  participant updateChat

  useVercelChat->>useChatModelSync: invoke(sessionId, chatId, model, setModel, hydrateFromDatabase)
  activate useChatModelSync

  alt hydrateFromDatabase enabled and session present
    useChatModelSync->>useHydrateChatModelFromDb: request hydration
    activate useHydrateChatModelFromDb
    useHydrateChatModelFromDb->>getSessionChat: GET /api/sessions/{sessionId}/chats/{chatId} (Authorization)
    getSessionChat-->>useHydrateChatModelFromDb: returns chat (modelId)
    useHydrateChatModelFromDb->>useChatModelSync: setModel(modelId), set hydrated
    deactivate useHydrateChatModelFromDb
  end

  useChatModelSync->>usePersistChatModelToDb: observe model changes
  activate usePersistChatModelToDb

  alt model changed and not already synced
    usePersistChatModelToDb->>updateChat: PATCH /api/sessions/{sessionId}/chats/{chatId} { modelId }
    updateChat-->>usePersistChatModelToDb: success/failure
    usePersistChatModelToDb->>useChatModelSync: update lastSyncedRef on success
  end

  deactivate usePersistChatModelToDb
  deactivate useChatModelSync
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • sweetmantech

Poem

A picker wakes from DB at dawn,
Sends a PATCH when choices move on,
Refs keep repeats far from sight,
Hydrated state turns wrong to right,
Sync hums softly — model’s song.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Solid & Clean Code ⚠️ Warning Code duplicates scope construction in 2 hooks (DRY violation), has error handling gap where missing token doesn't clear sync state, and violates OCP with hardcoded fields in buildPatchChatBody. Extract scope construction to a utility function; call clearPendingSync() when getAccessToken returns null; refactor buildPatchChatBody to be generic or inline it in updateChat.
✅ Passed checks (2 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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/persist-chat-model-id

Warning

Review ran into problems

🔥 Problems

Stopped waiting for pipeline failures after 30000ms. One of your pipelines takes longer than our 30000ms fetch window to run, so review may not consider pipeline-failure results for inline comments if any failures occurred after the fetch window. Increase the timeout if you want to wait longer or run a @coderabbit review after the pipeline has finished.


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 and usage tips.

@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: 93f3d454f5

ℹ️ 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/useVercelChat.ts Outdated
Comment on lines +84 to +88
usePersistChatModelId({
sessionId,
chatId: transportChatId,
model,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Avoid rewriting existing chats on mount

When an existing /sessions/.../chats/... page mounts, sessionId is already present and transportChatId is the route chat id, so this immediately PATCHes chats.model_id with the global RECOUP_MODEL localStorage value. Because the existing chat's persisted model is never loaded into model before this call, simply opening an old chat after selecting a different model elsewhere rewrites its DB model and subsequent workflow/regeneration billing uses the wrong model without any user action.

Useful? React with 👍 / 👎.

Comment thread hooks/usePersistChatModelId.ts Outdated
Comment on lines +50 to +55
await updateChat({
accessToken,
sessionId,
chatId,
modelId: model,
});

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 Guard against stale model PATCHes winning

If the user changes models quickly, an earlier effect can already be inside this updateChat call when cleanup sets cancelled; that does not abort the PATCH, so a slower request for the previous model can finish after the newer request and leave chats.model_id reverted to a stale value. This matters because the workflow reads the model from the DB, so the next send can run/bill against a model that no longer matches the picker.

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: 1

🧹 Nitpick comments (1)
lib/chats/updateChat.ts (1)

4-10: ⚡ Quick win

Encode the “at least one patch field” rule in UpdateChatParams.

title and modelId are both optional here, so callers can compile while passing neither and only fail at runtime when buildPatchChatBody throws. Tighten the type to require one of the two fields.

💡 Suggested type-level contract
-export type UpdateChatParams = {
+type UpdateChatBaseParams = {
   accessToken: string;
   sessionId: string;
   chatId: string;
-  title?: string;
-  modelId?: string;
 };
+
+type UpdateChatPatch =
+  | { title: string; modelId?: string }
+  | { title?: string; modelId: string };
+
+export type UpdateChatParams = UpdateChatBaseParams & UpdateChatPatch;

Also applies to: 46-46

🤖 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/chats/updateChat.ts` around lines 4 - 10, Update the UpdateChatParams
type so callers must provide at least one of title or modelId (instead of both
being optional) — encode this as a union merged with the common fields
(accessToken, sessionId, chatId) so the compiler enforces the “at least one
patch field” rule; update the declaration of UpdateChatParams and any duplicate
type declarations used elsewhere, and ensure call sites that pass through to
buildPatchChatBody still satisfy the new type.
🤖 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/usePersistChatModelId.ts`:
- Around line 33-61: The current usePersistChatModelId inflight dedupe
(lastSyncedRef + cancelled flag) can let older PATCHes overwrite newer choices;
change the logic in usePersistChatModelId to serialize updates per-scope or
implement abort+replay-latest: track an inflight operation map keyed by scope
(sessionId:chatId) and either (A) use an AbortController per inflight request
(created before calling updateChat via getAccessToken) and cancel the previous
request then immediately start a new one with the latest model, or (B) implement
a simple per-scope queue/lock that awaits the previous updateChat Promise, but
always checks a latestModelRef for the scope and only writes the newest model
(or replays the latest after the previous completes). Ensure you update
lastSyncedRef only after a successful write of that exact model, reference
functions/ids updateChat, getAccessToken, lastSyncedRef, and the scope
calculation to locate where to add the inflight map/abort logic.

---

Nitpick comments:
In `@lib/chats/updateChat.ts`:
- Around line 4-10: Update the UpdateChatParams type so callers must provide at
least one of title or modelId (instead of both being optional) — encode this as
a union merged with the common fields (accessToken, sessionId, chatId) so the
compiler enforces the “at least one patch field” rule; update the declaration of
UpdateChatParams and any duplicate type declarations used elsewhere, and ensure
call sites that pass through to buildPatchChatBody still satisfy the new type.
🪄 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: c44aaa41-14aa-48e0-b91f-948b503b5e43

📥 Commits

Reviewing files that changed from the base of the PR and between 2c0e643 and 93f3d45.

⛔ Files ignored due to path filters (1)
  • lib/chats/__tests__/buildPatchChatBody.test.ts is excluded by !**/*.test.* and included by lib/**
📒 Files selected for processing (4)
  • hooks/usePersistChatModelId.ts
  • hooks/useVercelChat.ts
  • lib/chats/buildPatchChatBody.ts
  • lib/chats/updateChat.ts

Comment thread hooks/usePersistChatModelId.ts Outdated
Comment on lines +33 to +61
const scope = `${sessionId}:${chatId}`;
if (
lastSyncedRef.current?.scope === scope &&
lastSyncedRef.current?.model === model
) {
return;
}

let cancelled = false;

void (async () => {
const accessToken = await getAccessToken();
if (!accessToken || cancelled) {
return;
}

try {
await updateChat({
accessToken,
sessionId,
chatId,
modelId: model,
});
if (!cancelled) {
lastSyncedRef.current = { scope, model };
}
} catch (error) {
console.error("[usePersistChatModelId] Failed to persist model:", error);
}

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.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

In-flight dedupe can leave chats.model_id stale relative to UI state.

Current cancellation only stops local ref updates; it does not stop an already-started PATCH. During fast model switches, an older request can finish after a newer choice and overwrite the DB with the wrong modelId. Also, failed writes are only logged and won’t retry until another dependency change occurs.

Please serialize per-chat model updates (or implement abort + replay-latest) so writes converge to the latest selected model.

🔧 Convergence-first approach (serialize latest desired model)
+// idea: keep one in-flight request per {sessionId, chatId}, queue only latest model
+const inFlightRef = useRef(false);
+const desiredRef = useRef<{ scope: string; model: string } | null>(null);

useEffect(() => {
  if (!sessionId) return;
  const scope = `${sessionId}:${chatId}`;
+ desiredRef.current = { scope, model };
+ if (inFlightRef.current) return;

  let cancelled = false;

  void (async () => {
+   inFlightRef.current = true;
+   while (!cancelled && desiredRef.current) {
+     const next = desiredRef.current;
+     desiredRef.current = null;
      const accessToken = await getAccessToken();
      if (!accessToken || cancelled) break;

      try {
        await updateChat({
          accessToken,
-         sessionId,
-         chatId,
-         modelId: model,
+         sessionId,
+         chatId,
+         modelId: next.model,
        });
-       if (!cancelled) {
-         lastSyncedRef.current = { scope, model };
-       }
+       if (!cancelled) lastSyncedRef.current = next;
      } catch (error) {
+       // optional: retry with backoff before exiting loop
        console.error("[usePersistChatModelId] Failed to persist model:", error);
+       break;
      }
+   }
+   inFlightRef.current = false;
  })();

  return () => {
    cancelled = true;
  };
}, [sessionId, chatId, model, getAccessToken]);
🤖 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/usePersistChatModelId.ts` around lines 33 - 61, The current
usePersistChatModelId inflight dedupe (lastSyncedRef + cancelled flag) can let
older PATCHes overwrite newer choices; change the logic in usePersistChatModelId
to serialize updates per-scope or implement abort+replay-latest: track an
inflight operation map keyed by scope (sessionId:chatId) and either (A) use an
AbortController per inflight request (created before calling updateChat via
getAccessToken) and cancel the previous request then immediately start a new one
with the latest model, or (B) implement a simple per-scope queue/lock that
awaits the previous updateChat Promise, but always checks a latestModelRef for
the scope and only writes the newest model (or replays the latest after the
previous completes). Ensure you update lastSyncedRef only after a successful
write of that exact model, reference functions/ids updateChat, getAccessToken,
lastSyncedRef, and the scope calculation to locate where to add the inflight
map/abort logic.

@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.

2 issues found across 5 files

Confidence score: 2/5

  • High-risk behavior is identified with strong confidence: both issues are severity 7-8/10 and can cause persisted chats.model_id to drift from the user’s intended selection, so this is not just cosmetic.
  • In hooks/usePersistChatModelId.ts, a race condition allows older in-flight PATCH responses to overwrite newer model choices, creating a concrete stale-write/regression risk for chat model persistence.
  • In hooks/useVercelChat.ts, opening an existing chat can immediately PATCH using the current global model rather than the chat’s stored model, which may unintentionally rewrite correct DB state on load.
  • Pay close attention to hooks/usePersistChatModelId.ts and hooks/useVercelChat.ts - request ordering and initial-load PATCH behavior can overwrite the intended persisted model.

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

Re-trigger cubic

Comment thread hooks/usePersistChatModelId.ts Outdated
Comment thread hooks/useVercelChat.ts Outdated

@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.

2 issues found across 4 files (changes from recent commits).

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="hooks/useChatModelSync.ts">

<violation number="1" location="hooks/useChatModelSync.ts:53">
P2: Early return on missing access token can leave `hydrated` false forever, blocking all later model persistence for that chat scope.</violation>
</file>

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

Re-trigger cubic

Comment thread hooks/useChatModelSync.ts
Comment thread hooks/useChatModelSync.ts Outdated
Comment on lines +53 to +72
if (!accessToken || cancelled) {
return;
}

try {
const { chat } = await getSessionChat(sessionId, chatId, accessToken);
if (cancelled) {
return;
}
if (chat.modelId) {
lastSyncedRef.current = { scope, model: chat.modelId };
setModel(chat.modelId);
}
} catch (error) {
console.error("[useChatModelSync] Failed to hydrate model:", error);
} finally {
if (!cancelled) {
setHydrated(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: Early return on missing access token can leave hydrated false forever, blocking all later model persistence for that chat scope.

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

<comment>Early return on missing access token can leave `hydrated` false forever, blocking all later model persistence for that chat scope.</comment>

<file context>
@@ -0,0 +1,113 @@
+
+    void (async () => {
+      const accessToken = await getAccessToken();
+      if (!accessToken || cancelled) {
+        return;
+      }
</file context>
Suggested change
if (!accessToken || cancelled) {
return;
}
try {
const { chat } = await getSessionChat(sessionId, chatId, accessToken);
if (cancelled) {
return;
}
if (chat.modelId) {
lastSyncedRef.current = { scope, model: chat.modelId };
setModel(chat.modelId);
}
} catch (error) {
console.error("[useChatModelSync] Failed to hydrate model:", error);
} finally {
if (!cancelled) {
setHydrated(true);
}
}
try {
const accessToken = await getAccessToken();
if (!accessToken || cancelled) {
return;
}
const { chat } = await getSessionChat(sessionId, chatId, accessToken);
if (cancelled) {
return;
}
if (chat.modelId) {
lastSyncedRef.current = { scope, model: chat.modelId };
setModel(chat.modelId);
}
} catch (error) {
console.error("[useChatModelSync] Failed to hydrate model:", error);
} finally {
if (!cancelled) {
setHydrated(true);
}
}

@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 1 file (changes from recent commits).

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

Re-trigger cubic

@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.

2 issues found across 3 files (changes from recent commits).

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="hooks/useChatModelSync.ts">

<violation number="1" location="hooks/useChatModelSync.ts:53">
P2: Early return on missing access token can leave `hydrated` false forever, blocking all later model persistence for that chat scope.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread hooks/useHydrateChatModelFromDb.ts
Comment thread hooks/usePersistChatModelToDb.ts

@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: 1

🤖 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/useHydrateChatModelFromDb.ts`:
- Around line 36-57: The async IIFE in useHydrateChatModelFromDb can exit early
on getAccessToken() returning null or getSessionChat throwing and never call
setHydrated(true), which blocks usePersistChatModelToDb; modify the IIFE to
guarantee setHydrated(true) is invoked in a finally-like path when not cancelled
(while preserving existing lastSyncedRef and setModel behavior), i.e., wrap the
try/catch with a try/catch/finally (or ensure a finally block) that calls
setHydrated(true) only if !cancelled so hydration state is always released even
on token or fetch failures; reference getAccessToken, getSessionChat,
setHydrated, cancelled, lastSyncedRef.current and setModel to locate the
changes.
🪄 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: 075777cb-ba5f-44be-8662-1699a2b07928

📥 Commits

Reviewing files that changed from the base of the PR and between 93f3d45 and ef02f3d.

📒 Files selected for processing (5)
  • hooks/useChatModelSync.ts
  • hooks/useHydrateChatModelFromDb.ts
  • hooks/usePersistChatModelToDb.ts
  • hooks/useVercelChat.ts
  • lib/chats/getSessionChat.ts

Comment thread hooks/useHydrateChatModelFromDb.ts

@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.

1 issue found across 2 files (changes from recent commits).

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="hooks/useHydrateChatModelFromDb.ts">

<violation number="1" location="hooks/useHydrateChatModelFromDb.ts:55">
P1: `setHydrated(true)` is unreachable when `getAccessToken()` returns null (early return) or when `getSessionChat` throws (jumps to catch). Since `usePersistChatModelToDb` is gated on `hydrated`, model changes will never persist after a token/fetch failure. Move `setHydrated(true)` into a `finally` block to ensure it's always called.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread hooks/useHydrateChatModelFromDb.ts Outdated
Comment on lines +55 to +56
} catch (error) {
console.error("[useHydrateChatModelFromDb] Failed to hydrate model:", error);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1: setHydrated(true) is unreachable when getAccessToken() returns null (early return) or when getSessionChat throws (jumps to catch). Since usePersistChatModelToDb is gated on hydrated, model changes will never persist after a token/fetch failure. Move setHydrated(true) into a finally block to ensure it's always called.

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

<comment>`setHydrated(true)` is unreachable when `getAccessToken()` returns null (early return) or when `getSessionChat` throws (jumps to catch). Since `usePersistChatModelToDb` is gated on `hydrated`, model changes will never persist after a token/fetch failure. Move `setHydrated(true)` into a `finally` block to ensure it's always called.</comment>

<file context>
@@ -48,12 +48,12 @@ export function useHydrateChatModelFromDb({
         if (!cancelled) {
           setHydrated(true);
         }
+      } catch (error) {
+        console.error("[useHydrateChatModelFromDb] Failed to hydrate model:", error);
       }
</file context>
Suggested change
} catch (error) {
console.error("[useHydrateChatModelFromDb] Failed to hydrate model:", error);
} catch (error) {
console.error("[useHydrateChatModelFromDb] Failed to hydrate model:", error);
} finally {
if (!cancelled) {
setHydrated(true);
}
}

@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.

1 issue found across 3 files (changes from recent commits).

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="hooks/useChatModelSync.ts">

<violation number="1" location="hooks/useChatModelSync.ts:53">
P2: Early return on missing access token can leave `hydrated` false forever, blocking all later model persistence for that chat scope.</violation>
</file>

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

<violation number="1" location="hooks/useHydrateChatModelFromDb.ts:55">
P1: `setHydrated(true)` is unreachable when `getAccessToken()` returns null (early return) or when `getSessionChat` throws (jumps to catch). Since `usePersistChatModelToDb` is gated on `hydrated`, model changes will never persist after a token/fetch failure. Move `setHydrated(true)` into a `finally` block to ensure it's always called.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread hooks/useHydrateChatModelFromDb.ts

@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 1 file (changes from recent commits).

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

Re-trigger cubic

…dating chat update parameters. Added `usePersistChatModelId` to `useVercelChat` for session and chat ID management. Updated `updateChat` to accept an optional `modelId` and modified the request body construction to include it.
…atModelSync` in `useVercelChat`. This change streamlines chat model synchronization by integrating model hydration logic directly into the new hook, enhancing code clarity and maintainability.
…mproved clarity in chat initialization. This change aligns with recent refactoring efforts to streamline chat management and enhance code maintainability.
…d `usePersistChatModelToDb` hooks for improved model synchronization. This change enhances code clarity and maintainability by separating hydration and persistence logic into dedicated hooks.
…ate management in `usePersistChatModelToDb`. The changes ensure that model hydration and persistence logic are more robust, with clearer error logging and state updates, enhancing overall reliability.
@sweetmantech

Copy link
Copy Markdown
Collaborator

Aligning this with the open-agents reference (we're following their model-passing pattern; api#638's request-body approach was closed in favor of this one).

open-agents resolves the model server-side from the persisted chat.modelId (apps/web/app/api/chat/route.ts:182-190), which our handleChatWorkflowStream.ts:93 already mirrors. The key to first-turn correctness in open-agents is that the chat row is created with the model already setapps/web/app/api/sessions/route.ts:266 creates the initial chat with modelId: preferences.defaultModelId (via lib/db/sessions.ts:92). The PATCH-on-change path is only for subsequent changes.

This PR currently ports the PATCH/hydrate half but not the creation-time write, so the headline bug ("every new chat bills haiku") isn't covered yet. Tracing this PR's own logic for a brand-new chat (no route chatId):

  • useVercelChat.ts: hydrateModelFromDatabase = Boolean(routeChatId) && routeChatId === transportChatIdfalse
  • useHydrateChatModelFromDb.ts: if (!hydrateFromDatabase || ...) return; → never calls setHydrated(true)
  • usePersistChatModelToDb.ts: if (!sessionId || !hydrated) return; → PATCH never fires

So chats.model_id is never written before the first send → the api reads the column default ('anthropic/claude-haiku-4.5', database/.../20260501000000_open_agents_sessions_and_chats.sql:57) → first turn bills haiku.

To match open-agents, add the creation-time write: have POST /api/sessions (recoup lib/sessions/createSessionHandler.ts:69-73) accept a modelId (the user's selected/default model) and pass it into insertChat({ …, model_id }), exactly like open-agents sessions/route.ts:266. Then this PR's PATCH/hydrate handles changes on existing chats.

Secondary (lower priority): the send path doesn't await the persist chain (handleSubmit(event) fires independently of persistChainRef.current), so "change model → send fast" on an existing chat can still race the PATCH. open-agents leans on the creation-time set rather than racing a PATCH, so fixing #1 mostly removes the sharp edge.

Repro to confirm before merge: new chat → pick a non-default model → send first message → check usage_events.model_id. Expect it to currently show haiku.

@sweetmantech

Copy link
Copy Markdown
Collaborator

E2E test on this preview — selected model is not honored on a new chat's first turn

Tested https://chat-git-fix-persist-chat-model-id-recoup.vercel.app (head 3acaf5eb), logged in, new chat → opened the picker → selected Claude Opus 4.6 → sent the first message ("Reply with exactly one word: pong").

Result — picked Opus 4.6, billed GPT-5.4 Mini:

value
Session / chat 3a6ddba9-4d47-498c-8083-f803bc38b3d9 / 226cb5a1-7d0c-4772-99df-8dd528265e56
Model I selected in the UI Claude Opus 4.6 (anthropic/claude-opus-4.6)
chats.model_id persisted openai/gpt-5.4-mini
usage_events billed (22:22:27Z, the pong turn, 3727 in / 5 out) openai/gpt-5.4-mini

Acceptance criterion (issue #1767) is "pick a non-default model → send → usage_events.model_id shows what you selected." It showed openai/gpt-5.4-mini, not anthropic/claude-opus-4.6fails.

Diagnosis

Worth noting it's a bit different from "always bills haiku": it did not fall to the DB column default (anthropic/claude-haiku-4.5) — it captured the picker's initial page-load default (gpt-5.4-mini), not my deliberate change to Opus made right before sending. So a model change made before the first send is lost. That matches the earlier analysis: the send path doesn't await the persist chain, and the new-chat path has no creation-time write of the actual selection.

What's still needed (open-agents-faithful)

Set chats.model_id at chat creation to the user's currently selected model, server-side — mirror open-agents apps/web/app/api/sessions/route.ts:266 (modelId: preferences.defaultModelId, via lib/db/sessions.ts:92). Today recoup's api/lib/sessions/createSessionHandler.ts:69-73 inserts the initial chat with no model_id. Once the first turn reads the user's selection race-free, this PR's PATCH/hydrate handles changes on existing chats.

(Test created a real 1¢ chat in the Justin Bieber workspace.)

sweetmantech added a commit that referenced this pull request Jun 4, 2026
#1781)

* fix(chat): persist selected model before send so the workflow bills it

The chat-workflow path bills the model it reads from chats.model_id at
request time. The picker selection was never written there, so every new
chat (and any model change) billed the chats.model_id default instead of
the chosen model.

Before sending, await a PATCH of the selected model to
chats.model_id when it changed for the active chat (shouldPersistChatModel
guards redundant writes). The chat row already exists from new-chat
bootstrap, so this covers the first turn race-free without an api change.

Reuses buildPatchChatBody + updateChat(modelId) (originally from #1779).

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

* refactor(chat): extract model-persist-before-send into usePersistSelectedModel

Addresses OCP/SRP review feedback on #1781: instead of inlining the
persist-before-send block in useVercelChat, encapsulate it in a dedicated
usePersistSelectedModel hook (owns the lastPersisted ref + guard + PATCH).
useVercelChat now just calls `await persistSelectedModel()` before send.

Behavior unchanged; pure decision logic still covered by
shouldPersistChatModel tests.

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

* fix(chat): persist selected model on retry/edit + append paths

Addresses review feedback on #1781:
- Retry/Edit call reload() → regenerate (and append() calls sendMessage)
  bypassing handleSubmit, so the selected model was not persisted before
  those sends — the regenerated turn could bill the previous/default model.
  Now await persistSelectedModel() in handleReload and append too.
- Clarify updateChat JSDoc: it patches title and/or modelId (not just rename).

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 Jun 4, 2026
…ms (#1782)

* refactor(chat): update chat components to use chatId instead of roomId (#1765)

* refactor(chat): update chat components to use chatId instead of roomId

This commit removes the InstantChatRoom component and updates various hooks and components to replace references of roomId with chatId. The changes ensure that chat functionality is aligned with the new session-scoped architecture, improving consistency across the chat system. Additionally, the sessionId is now required in several components to streamline the chat transport process.

* refactor(chat): integrate sessionId and update chat path utilities

This commit enhances the chat components by introducing the `sessionId` in various hooks and updating the URL handling to utilize new utility functions for generating chat paths. The changes improve the consistency and maintainability of chat navigation, ensuring that the application correctly reflects the session-scoped architecture.

* fix(chat): ensure newRoomId is treated as a string in URL handling

This commit updates the `useCreateArtistTool` hook to explicitly cast `result.newRoomId` to a string when generating the chat path. This change prevents potential type-related issues and ensures consistent URL formatting. Additionally, it adds an import statement for testing utilities in the chat paths test file, enhancing test coverage and maintainability.

* refactor(Header, OrganizationProvider): remove usePathname and replace with window.location.pathname

This commit removes the usePathname hook from both Artist.tsx and OrganizationProvider.tsx, replacing it with direct access to window.location.pathname. This change simplifies the code and maintains the intended navigation behavior without relying on Next.js's router for pathname management.

* refactor(chat): split chatPaths per SRP, drop dead legacy branch, simplify useParams

- Remove dead `/chat/` branch from isActiveChatRoomPath — this PR makes
  /chat/{id} 404, so treating it as an active chat room contradicted the
  PR's own intent and could never fire in production.
- Split lib/chat/chatPaths.ts into one-function-per-file (getChatPath,
  getChatUrl, isActiveChatRoomPath) per repo SRP convention; mirror tests.
- Simplify useVercelChat to `const { chatId } = useParams<{ chatId?: string }>()`
  (KISS), consistent with chat.tsx.

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

---------

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

* feat(chat): defer new-chat bootstrap wait from spinner to send button (#1767) (#1774)

* feat(chat): defer new-chat bootstrap wait from spinner to send button (#1767)

New chats landing on `/` or `/chat` blocked the whole view on a
full-screen spinner while `POST /api/sessions` + `POST /api/sandbox`
resolved (~12s cold, dominated by sandbox provisioning). Render `<Chat>`
immediately with a typeable input instead and provision in parallel; the
Send button stays disabled with a "Preparing your workspace…" cue until
the api-minted `sessionId` + `chatId` land, so the workflow transport
never fires without the ids its validator requires.

- NewChatBootstrap: render <Chat> eagerly with a stable placeholder chat
  id (survives the preparing → ready transition); drop the spinner.
- Thread `sessionId?`, `workflowChatId`, `isBootstrapPreparing` through
  Chat → VercelChatProvider → useVercelChat (provider kept inline).
- useVercelChat: `transportChatId = workflowChatId ?? id` drives the
  transport, optimistic conversation, and URL; only load persisted
  history on a canonical route so no spinner flashes over the input
  during the ready transition.
- ChatInput: gate Send on `isBootstrapPreparing`; input stays typeable.
- useChatTransport / useMessageLoader: sessionId optional + guard.
- useCreateArtistTool: guard the one context sessionId consumer.

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

* fix(chat): send live sessionId/chatId from workflow transport (#1767)

The new-chat flow mounts <Chat> (and useChat) during provisioning with
sessionId undefined + a placeholder chatId. useChat captures the transport
from that mount render and does not swap to the rebuilt instance when the
ids later resolve — so the first send POSTed `sessionId: undefined` to
/api/chat/workflow and got a 400 (the URL rewrote correctly because it
reads the live prop, not the transport).

Read the ids from refs inside transport.body() so a single stable
transport instance always sends the values current as of the request,
regardless of useChat's stale capture. Removed chatId/sessionId from the
memo deps since rebuilding the instance never helped.

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

* feat(chat): replace preparing-text cue with a workspace status dot (#1767)

Swap the flashing "Preparing your workspace…" toolbar text for a
stoplight status dot in the top-right of the chat input: red = off
(provisioning failed / unavailable), yellow = provisioning, green =
ready. Hover shows context via the shadcn tooltip.

- New `WorkspaceStatusIndicator` component in its own file (SRP) — a pure
  display component driven by a `status` prop, built on the existing
  shadcn-based `common/Tooltip`, so it's open for reuse without change.
- Thread `workspaceStatus: "off" | "provisioning" | "ready"` through
  Chat → VercelChatProvider → context (replacing the `isBootstrapPreparing`
  boolean); Send is gated while it's not `ready`.
- NewChatBootstrap now derives the status and renders `<Chat>` even on a
  provisioning error (red dot + disabled Send, input still visible)
  instead of swapping in a full-screen error view.

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

* chore(chat): simplify provisioning tooltip copy (#1767)

Shorten the yellow/provisioning workspace-status tooltip to
"Preparing your workspace".

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(chat): drop legacy GET /api/chats/{id}/artist caller (#1767) (#1768)

* fix(chat): drop legacy GET /api/chats/{id}/artist caller (#1767)

* fix(chat): reactive conversations cache + user-scoped session query

* refactor(chat): drop dead /chat/:id artist cache fallback (post-#1765)

The conversations-cache fallback in useArtistFromChat only existed for the
legacy /chat/:id deep link (chatId but no sessionId). That route was
removed in #1765 — every chat is now reached via the canonical
/sessions/{sessionId}/chats/{chatId} URL, so sessionId is always present.

Reduce to the session path: GET /api/sessions/{sessionId} -> session.artistId
-> select. Removes useSyncExternalStore reactive-cache machinery,
findArtistIdInConversationsCache (+ its test), and the unused chatId param.

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

* refactor(chat): validate getSessionById response with zod (PR #1768 review)

Replace type-casting with boundary zod validation, matching the
getConversations convention. Drops the `as GetSessionByIdResponse`/
`as GetSessionByIdErrorResponse` casts in favor of safeParse/parse.

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

* refactor(chat): extract shared safeJsonParse into lib/api (PR #1768 review)

Replace the locally-defined safeJsonParse in getSessionById with a
shared helper in lib/api/, alongside the other HTTP helpers. DRY/SRP.

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

---------

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

* fix(chat): persist selected model before send so the workflow bills it (#1781)

* fix(chat): persist selected model before send so the workflow bills it

The chat-workflow path bills the model it reads from chats.model_id at
request time. The picker selection was never written there, so every new
chat (and any model change) billed the chats.model_id default instead of
the chosen model.

Before sending, await a PATCH of the selected model to
chats.model_id when it changed for the active chat (shouldPersistChatModel
guards redundant writes). The chat row already exists from new-chat
bootstrap, so this covers the first turn race-free without an api change.

Reuses buildPatchChatBody + updateChat(modelId) (originally from #1779).

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

* refactor(chat): extract model-persist-before-send into usePersistSelectedModel

Addresses OCP/SRP review feedback on #1781: instead of inlining the
persist-before-send block in useVercelChat, encapsulate it in a dedicated
usePersistSelectedModel hook (owns the lastPersisted ref + guard + PATCH).
useVercelChat now just calls `await persistSelectedModel()` before send.

Behavior unchanged; pure decision logic still covered by
shouldPersistChatModel tests.

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

* fix(chat): persist selected model on retry/edit + append paths

Addresses review feedback on #1781:
- Retry/Edit call reload() → regenerate (and append() calls sendMessage)
  bypassing handleSubmit, so the selected model was not persisted before
  those sends — the regenerated turn could bill the previous/default model.
  Now await persistSelectedModel() in handleReload and append too.
- Clarify updateChat JSDoc: it patches title and/or modelId (not just rename).

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: ahmednahima0-beep <ahmednahima0@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…`authenticated` and `model` parameters to improve state management and error handling during model hydration. This change ensures that the hydration process is more robust and prevents unnecessary updates when authentication fails.
…el` parameter and improve state management by introducing `pickerModelOnOpen`. This change enhances the logic for handling model hydration failures, ensuring that user-initiated actions are not blocked unnecessarily.
sweetmantech added a commit that referenced this pull request Jun 5, 2026
…at /api/chat repoint (chat#1783) (#1784)

* refactor(chat): update chat components to use chatId instead of roomId (#1765)

* refactor(chat): update chat components to use chatId instead of roomId

This commit removes the InstantChatRoom component and updates various hooks and components to replace references of roomId with chatId. The changes ensure that chat functionality is aligned with the new session-scoped architecture, improving consistency across the chat system. Additionally, the sessionId is now required in several components to streamline the chat transport process.

* refactor(chat): integrate sessionId and update chat path utilities

This commit enhances the chat components by introducing the `sessionId` in various hooks and updating the URL handling to utilize new utility functions for generating chat paths. The changes improve the consistency and maintainability of chat navigation, ensuring that the application correctly reflects the session-scoped architecture.

* fix(chat): ensure newRoomId is treated as a string in URL handling

This commit updates the `useCreateArtistTool` hook to explicitly cast `result.newRoomId` to a string when generating the chat path. This change prevents potential type-related issues and ensures consistent URL formatting. Additionally, it adds an import statement for testing utilities in the chat paths test file, enhancing test coverage and maintainability.

* refactor(Header, OrganizationProvider): remove usePathname and replace with window.location.pathname

This commit removes the usePathname hook from both Artist.tsx and OrganizationProvider.tsx, replacing it with direct access to window.location.pathname. This change simplifies the code and maintains the intended navigation behavior without relying on Next.js's router for pathname management.

* refactor(chat): split chatPaths per SRP, drop dead legacy branch, simplify useParams

- Remove dead `/chat/` branch from isActiveChatRoomPath — this PR makes
  /chat/{id} 404, so treating it as an active chat room contradicted the
  PR's own intent and could never fire in production.
- Split lib/chat/chatPaths.ts into one-function-per-file (getChatPath,
  getChatUrl, isActiveChatRoomPath) per repo SRP convention; mirror tests.
- Simplify useVercelChat to `const { chatId } = useParams<{ chatId?: string }>()`
  (KISS), consistent with chat.tsx.

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

---------

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

* feat(chat): defer new-chat bootstrap wait from spinner to send button (#1767) (#1774)

* feat(chat): defer new-chat bootstrap wait from spinner to send button (#1767)

New chats landing on `/` or `/chat` blocked the whole view on a
full-screen spinner while `POST /api/sessions` + `POST /api/sandbox`
resolved (~12s cold, dominated by sandbox provisioning). Render `<Chat>`
immediately with a typeable input instead and provision in parallel; the
Send button stays disabled with a "Preparing your workspace…" cue until
the api-minted `sessionId` + `chatId` land, so the workflow transport
never fires without the ids its validator requires.

- NewChatBootstrap: render <Chat> eagerly with a stable placeholder chat
  id (survives the preparing → ready transition); drop the spinner.
- Thread `sessionId?`, `workflowChatId`, `isBootstrapPreparing` through
  Chat → VercelChatProvider → useVercelChat (provider kept inline).
- useVercelChat: `transportChatId = workflowChatId ?? id` drives the
  transport, optimistic conversation, and URL; only load persisted
  history on a canonical route so no spinner flashes over the input
  during the ready transition.
- ChatInput: gate Send on `isBootstrapPreparing`; input stays typeable.
- useChatTransport / useMessageLoader: sessionId optional + guard.
- useCreateArtistTool: guard the one context sessionId consumer.

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

* fix(chat): send live sessionId/chatId from workflow transport (#1767)

The new-chat flow mounts <Chat> (and useChat) during provisioning with
sessionId undefined + a placeholder chatId. useChat captures the transport
from that mount render and does not swap to the rebuilt instance when the
ids later resolve — so the first send POSTed `sessionId: undefined` to
/api/chat/workflow and got a 400 (the URL rewrote correctly because it
reads the live prop, not the transport).

Read the ids from refs inside transport.body() so a single stable
transport instance always sends the values current as of the request,
regardless of useChat's stale capture. Removed chatId/sessionId from the
memo deps since rebuilding the instance never helped.

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

* feat(chat): replace preparing-text cue with a workspace status dot (#1767)

Swap the flashing "Preparing your workspace…" toolbar text for a
stoplight status dot in the top-right of the chat input: red = off
(provisioning failed / unavailable), yellow = provisioning, green =
ready. Hover shows context via the shadcn tooltip.

- New `WorkspaceStatusIndicator` component in its own file (SRP) — a pure
  display component driven by a `status` prop, built on the existing
  shadcn-based `common/Tooltip`, so it's open for reuse without change.
- Thread `workspaceStatus: "off" | "provisioning" | "ready"` through
  Chat → VercelChatProvider → context (replacing the `isBootstrapPreparing`
  boolean); Send is gated while it's not `ready`.
- NewChatBootstrap now derives the status and renders `<Chat>` even on a
  provisioning error (red dot + disabled Send, input still visible)
  instead of swapping in a full-screen error view.

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

* chore(chat): simplify provisioning tooltip copy (#1767)

Shorten the yellow/provisioning workspace-status tooltip to
"Preparing your workspace".

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(chat): drop legacy GET /api/chats/{id}/artist caller (#1767) (#1768)

* fix(chat): drop legacy GET /api/chats/{id}/artist caller (#1767)

* fix(chat): reactive conversations cache + user-scoped session query

* refactor(chat): drop dead /chat/:id artist cache fallback (post-#1765)

The conversations-cache fallback in useArtistFromChat only existed for the
legacy /chat/:id deep link (chatId but no sessionId). That route was
removed in #1765 — every chat is now reached via the canonical
/sessions/{sessionId}/chats/{chatId} URL, so sessionId is always present.

Reduce to the session path: GET /api/sessions/{sessionId} -> session.artistId
-> select. Removes useSyncExternalStore reactive-cache machinery,
findArtistIdInConversationsCache (+ its test), and the unused chatId param.

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

* refactor(chat): validate getSessionById response with zod (PR #1768 review)

Replace type-casting with boundary zod validation, matching the
getConversations convention. Drops the `as GetSessionByIdResponse`/
`as GetSessionByIdErrorResponse` casts in favor of safeParse/parse.

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

* refactor(chat): extract shared safeJsonParse into lib/api (PR #1768 review)

Replace the locally-defined safeJsonParse in getSessionById with a
shared helper in lib/api/, alongside the other HTTP helpers. DRY/SRP.

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

---------

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

* fix(chat): persist selected model before send so the workflow bills it (#1781)

* fix(chat): persist selected model before send so the workflow bills it

The chat-workflow path bills the model it reads from chats.model_id at
request time. The picker selection was never written there, so every new
chat (and any model change) billed the chats.model_id default instead of
the chosen model.

Before sending, await a PATCH of the selected model to
chats.model_id when it changed for the active chat (shouldPersistChatModel
guards redundant writes). The chat row already exists from new-chat
bootstrap, so this covers the first turn race-free without an api change.

Reuses buildPatchChatBody + updateChat(modelId) (originally from #1779).

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

* refactor(chat): extract model-persist-before-send into usePersistSelectedModel

Addresses OCP/SRP review feedback on #1781: instead of inlining the
persist-before-send block in useVercelChat, encapsulate it in a dedicated
usePersistSelectedModel hook (owns the lastPersisted ref + guard + PATCH).
useVercelChat now just calls `await persistSelectedModel()` before send.

Behavior unchanged; pure decision logic still covered by
shouldPersistChatModel tests.

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

* fix(chat): persist selected model on retry/edit + append paths

Addresses review feedback on #1781:
- Retry/Edit call reload() → regenerate (and append() calls sendMessage)
  bypassing handleSubmit, so the selected model was not persisted before
  those sends — the regenerated turn could bill the previous/default model.
  Now await persistSelectedModel() in handleReload and append too.
- Clarify updateChat JSDoc: it patches title and/or modelId (not just rename).

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(chat): repoint chat transport to canonical POST /api/chat (#1783)

Step 3 of the /api/chat/workflow → /api/chat rename (chat#1767), against
the merged docs contract (docs#235) and the api endpoint (api#645, on test).

useChatTransport now POSTs to ${baseUrl}/api/chat instead of
/api/chat/workflow. Comment-only refs in VercelChatProvider updated too.

⚠️ Deploy coordination: requires api#645 (which removes /api/chat/workflow)
to be live in the same environment. Must deploy together with the api
promotion and the open-agents repoint (step 4), or chat 404s.

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

---------

Co-authored-by: ahmednahima0-beep <ahmednahima0@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
sweetmantech added a commit that referenced this pull request Jun 18, 2026
* refactor(chat): update chat components to use chatId instead of roomId (#1765)

* refactor(chat): update chat components to use chatId instead of roomId

This commit removes the InstantChatRoom component and updates various hooks and components to replace references of roomId with chatId. The changes ensure that chat functionality is aligned with the new session-scoped architecture, improving consistency across the chat system. Additionally, the sessionId is now required in several components to streamline the chat transport process.

* refactor(chat): integrate sessionId and update chat path utilities

This commit enhances the chat components by introducing the `sessionId` in various hooks and updating the URL handling to utilize new utility functions for generating chat paths. The changes improve the consistency and maintainability of chat navigation, ensuring that the application correctly reflects the session-scoped architecture.

* fix(chat): ensure newRoomId is treated as a string in URL handling

This commit updates the `useCreateArtistTool` hook to explicitly cast `result.newRoomId` to a string when generating the chat path. This change prevents potential type-related issues and ensures consistent URL formatting. Additionally, it adds an import statement for testing utilities in the chat paths test file, enhancing test coverage and maintainability.

* refactor(Header, OrganizationProvider): remove usePathname and replace with window.location.pathname

This commit removes the usePathname hook from both Artist.tsx and OrganizationProvider.tsx, replacing it with direct access to window.location.pathname. This change simplifies the code and maintains the intended navigation behavior without relying on Next.js's router for pathname management.

* refactor(chat): split chatPaths per SRP, drop dead legacy branch, simplify useParams

- Remove dead `/chat/` branch from isActiveChatRoomPath — this PR makes
  /chat/{id} 404, so treating it as an active chat room contradicted the
  PR's own intent and could never fire in production.
- Split lib/chat/chatPaths.ts into one-function-per-file (getChatPath,
  getChatUrl, isActiveChatRoomPath) per repo SRP convention; mirror tests.
- Simplify useVercelChat to `const { chatId } = useParams<{ chatId?: string }>()`
  (KISS), consistent with chat.tsx.

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

---------

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

* feat(chat): defer new-chat bootstrap wait from spinner to send button (#1767) (#1774)

* feat(chat): defer new-chat bootstrap wait from spinner to send button (#1767)

New chats landing on `/` or `/chat` blocked the whole view on a
full-screen spinner while `POST /api/sessions` + `POST /api/sandbox`
resolved (~12s cold, dominated by sandbox provisioning). Render `<Chat>`
immediately with a typeable input instead and provision in parallel; the
Send button stays disabled with a "Preparing your workspace…" cue until
the api-minted `sessionId` + `chatId` land, so the workflow transport
never fires without the ids its validator requires.

- NewChatBootstrap: render <Chat> eagerly with a stable placeholder chat
  id (survives the preparing → ready transition); drop the spinner.
- Thread `sessionId?`, `workflowChatId`, `isBootstrapPreparing` through
  Chat → VercelChatProvider → useVercelChat (provider kept inline).
- useVercelChat: `transportChatId = workflowChatId ?? id` drives the
  transport, optimistic conversation, and URL; only load persisted
  history on a canonical route so no spinner flashes over the input
  during the ready transition.
- ChatInput: gate Send on `isBootstrapPreparing`; input stays typeable.
- useChatTransport / useMessageLoader: sessionId optional + guard.
- useCreateArtistTool: guard the one context sessionId consumer.

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

* fix(chat): send live sessionId/chatId from workflow transport (#1767)

The new-chat flow mounts <Chat> (and useChat) during provisioning with
sessionId undefined + a placeholder chatId. useChat captures the transport
from that mount render and does not swap to the rebuilt instance when the
ids later resolve — so the first send POSTed `sessionId: undefined` to
/api/chat/workflow and got a 400 (the URL rewrote correctly because it
reads the live prop, not the transport).

Read the ids from refs inside transport.body() so a single stable
transport instance always sends the values current as of the request,
regardless of useChat's stale capture. Removed chatId/sessionId from the
memo deps since rebuilding the instance never helped.

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

* feat(chat): replace preparing-text cue with a workspace status dot (#1767)

Swap the flashing "Preparing your workspace…" toolbar text for a
stoplight status dot in the top-right of the chat input: red = off
(provisioning failed / unavailable), yellow = provisioning, green =
ready. Hover shows context via the shadcn tooltip.

- New `WorkspaceStatusIndicator` component in its own file (SRP) — a pure
  display component driven by a `status` prop, built on the existing
  shadcn-based `common/Tooltip`, so it's open for reuse without change.
- Thread `workspaceStatus: "off" | "provisioning" | "ready"` through
  Chat → VercelChatProvider → context (replacing the `isBootstrapPreparing`
  boolean); Send is gated while it's not `ready`.
- NewChatBootstrap now derives the status and renders `<Chat>` even on a
  provisioning error (red dot + disabled Send, input still visible)
  instead of swapping in a full-screen error view.

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

* chore(chat): simplify provisioning tooltip copy (#1767)

Shorten the yellow/provisioning workspace-status tooltip to
"Preparing your workspace".

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(chat): drop legacy GET /api/chats/{id}/artist caller (#1767) (#1768)

* fix(chat): drop legacy GET /api/chats/{id}/artist caller (#1767)

* fix(chat): reactive conversations cache + user-scoped session query

* refactor(chat): drop dead /chat/:id artist cache fallback (post-#1765)

The conversations-cache fallback in useArtistFromChat only existed for the
legacy /chat/:id deep link (chatId but no sessionId). That route was
removed in #1765 — every chat is now reached via the canonical
/sessions/{sessionId}/chats/{chatId} URL, so sessionId is always present.

Reduce to the session path: GET /api/sessions/{sessionId} -> session.artistId
-> select. Removes useSyncExternalStore reactive-cache machinery,
findArtistIdInConversationsCache (+ its test), and the unused chatId param.

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

* refactor(chat): validate getSessionById response with zod (PR #1768 review)

Replace type-casting with boundary zod validation, matching the
getConversations convention. Drops the `as GetSessionByIdResponse`/
`as GetSessionByIdErrorResponse` casts in favor of safeParse/parse.

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

* refactor(chat): extract shared safeJsonParse into lib/api (PR #1768 review)

Replace the locally-defined safeJsonParse in getSessionById with a
shared helper in lib/api/, alongside the other HTTP helpers. DRY/SRP.

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

---------

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

* refactor(chat): replace chatId with transportChatId in message editor and hooks

This commit updates the MessageEditor component and the useVercelChat hook to utilize transportChatId instead of the previous chatId. This change enhances the consistency of chat-related API calls and aligns with the new session-scoped architecture, ensuring that the correct chat identifier is used for operations like deleting trailing messages.

* fix(chat): persist selected model before send so the workflow bills it (#1781)

* fix(chat): persist selected model before send so the workflow bills it

The chat-workflow path bills the model it reads from chats.model_id at
request time. The picker selection was never written there, so every new
chat (and any model change) billed the chats.model_id default instead of
the chosen model.

Before sending, await a PATCH of the selected model to
chats.model_id when it changed for the active chat (shouldPersistChatModel
guards redundant writes). The chat row already exists from new-chat
bootstrap, so this covers the first turn race-free without an api change.

Reuses buildPatchChatBody + updateChat(modelId) (originally from #1779).

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

* refactor(chat): extract model-persist-before-send into usePersistSelectedModel

Addresses OCP/SRP review feedback on #1781: instead of inlining the
persist-before-send block in useVercelChat, encapsulate it in a dedicated
usePersistSelectedModel hook (owns the lastPersisted ref + guard + PATCH).
useVercelChat now just calls `await persistSelectedModel()` before send.

Behavior unchanged; pure decision logic still covered by
shouldPersistChatModel tests.

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

* fix(chat): persist selected model on retry/edit + append paths

Addresses review feedback on #1781:
- Retry/Edit call reload() → regenerate (and append() calls sendMessage)
  bypassing handleSubmit, so the selected model was not persisted before
  those sends — the regenerated turn could bill the previous/default model.
  Now await persistSelectedModel() in handleReload and append too.
- Clarify updateChat JSDoc: it patches title and/or modelId (not just rename).

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(chat): repoint chat transport to canonical POST /api/chat (#1783)

Step 3 of the /api/chat/workflow → /api/chat rename (chat#1767), against
the merged docs contract (docs#235) and the api endpoint (api#645, on test).

useChatTransport now POSTs to ${baseUrl}/api/chat instead of
/api/chat/workflow. Comment-only refs in VercelChatProvider updated too.

⚠️ Deploy coordination: requires api#645 (which removes /api/chat/workflow)
to be live in the same environment. Must deploy together with the api
promotion and the open-agents repoint (step 4), or chat 404s.

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

* Revert "refactor(chat): replace chatId with transportChatId in message editor and hooks"

This reverts commit 9f34b11.

* chore: upgrade @privy-io/react-auth v1.88.4 → v3.31.0 (#1802)

* chore: upgrade @privy-io/react-auth v1.88.4 -> v3.31.0

Prerequisite for cross-subdomain (cookie) auth in #1801:
matches marketing's ^3.31.0 so both surfaces run a cookie-capable SDK.

Migration surface is minimal — every @privy-io import in chat is usePrivy
or the PrivyProvider component (audited all 48 sites); none of the
v3-renamed/removed hooks (useLoginToFrame, useSignAuthorization,
useSolanaWallets, useWallets, useLogin/useLogout callbacks) are used, and
usePrivy()'s return shape (login/logout/authenticated/user/getAccessToken/
ready) plus user.email.address / user.wallet.address are unchanged in v3.

Only code change: embeddedWallets.createOnLogin moved under a per-chain key
(v3 breaking change) -> embeddedWallets.ethereum.createOnLogin.

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

* Update providers/PrivyProvider.tsx

---------

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

---------

Co-authored-by: ahmednahima0-beep <ahmednahima0@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: pradipthaadhi <yulius.upwork@gmail.com>
sweetmantech added a commit that referenced this pull request Jun 18, 2026
* refactor(chat): update chat components to use chatId instead of roomId (#1765)

* refactor(chat): update chat components to use chatId instead of roomId

This commit removes the InstantChatRoom component and updates various hooks and components to replace references of roomId with chatId. The changes ensure that chat functionality is aligned with the new session-scoped architecture, improving consistency across the chat system. Additionally, the sessionId is now required in several components to streamline the chat transport process.

* refactor(chat): integrate sessionId and update chat path utilities

This commit enhances the chat components by introducing the `sessionId` in various hooks and updating the URL handling to utilize new utility functions for generating chat paths. The changes improve the consistency and maintainability of chat navigation, ensuring that the application correctly reflects the session-scoped architecture.

* fix(chat): ensure newRoomId is treated as a string in URL handling

This commit updates the `useCreateArtistTool` hook to explicitly cast `result.newRoomId` to a string when generating the chat path. This change prevents potential type-related issues and ensures consistent URL formatting. Additionally, it adds an import statement for testing utilities in the chat paths test file, enhancing test coverage and maintainability.

* refactor(Header, OrganizationProvider): remove usePathname and replace with window.location.pathname

This commit removes the usePathname hook from both Artist.tsx and OrganizationProvider.tsx, replacing it with direct access to window.location.pathname. This change simplifies the code and maintains the intended navigation behavior without relying on Next.js's router for pathname management.

* refactor(chat): split chatPaths per SRP, drop dead legacy branch, simplify useParams

- Remove dead `/chat/` branch from isActiveChatRoomPath — this PR makes
  /chat/{id} 404, so treating it as an active chat room contradicted the
  PR's own intent and could never fire in production.
- Split lib/chat/chatPaths.ts into one-function-per-file (getChatPath,
  getChatUrl, isActiveChatRoomPath) per repo SRP convention; mirror tests.
- Simplify useVercelChat to `const { chatId } = useParams<{ chatId?: string }>()`
  (KISS), consistent with chat.tsx.

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

---------

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

* feat(chat): defer new-chat bootstrap wait from spinner to send button (#1767) (#1774)

* feat(chat): defer new-chat bootstrap wait from spinner to send button (#1767)

New chats landing on `/` or `/chat` blocked the whole view on a
full-screen spinner while `POST /api/sessions` + `POST /api/sandbox`
resolved (~12s cold, dominated by sandbox provisioning). Render `<Chat>`
immediately with a typeable input instead and provision in parallel; the
Send button stays disabled with a "Preparing your workspace…" cue until
the api-minted `sessionId` + `chatId` land, so the workflow transport
never fires without the ids its validator requires.

- NewChatBootstrap: render <Chat> eagerly with a stable placeholder chat
  id (survives the preparing → ready transition); drop the spinner.
- Thread `sessionId?`, `workflowChatId`, `isBootstrapPreparing` through
  Chat → VercelChatProvider → useVercelChat (provider kept inline).
- useVercelChat: `transportChatId = workflowChatId ?? id` drives the
  transport, optimistic conversation, and URL; only load persisted
  history on a canonical route so no spinner flashes over the input
  during the ready transition.
- ChatInput: gate Send on `isBootstrapPreparing`; input stays typeable.
- useChatTransport / useMessageLoader: sessionId optional + guard.
- useCreateArtistTool: guard the one context sessionId consumer.

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

* fix(chat): send live sessionId/chatId from workflow transport (#1767)

The new-chat flow mounts <Chat> (and useChat) during provisioning with
sessionId undefined + a placeholder chatId. useChat captures the transport
from that mount render and does not swap to the rebuilt instance when the
ids later resolve — so the first send POSTed `sessionId: undefined` to
/api/chat/workflow and got a 400 (the URL rewrote correctly because it
reads the live prop, not the transport).

Read the ids from refs inside transport.body() so a single stable
transport instance always sends the values current as of the request,
regardless of useChat's stale capture. Removed chatId/sessionId from the
memo deps since rebuilding the instance never helped.

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

* feat(chat): replace preparing-text cue with a workspace status dot (#1767)

Swap the flashing "Preparing your workspace…" toolbar text for a
stoplight status dot in the top-right of the chat input: red = off
(provisioning failed / unavailable), yellow = provisioning, green =
ready. Hover shows context via the shadcn tooltip.

- New `WorkspaceStatusIndicator` component in its own file (SRP) — a pure
  display component driven by a `status` prop, built on the existing
  shadcn-based `common/Tooltip`, so it's open for reuse without change.
- Thread `workspaceStatus: "off" | "provisioning" | "ready"` through
  Chat → VercelChatProvider → context (replacing the `isBootstrapPreparing`
  boolean); Send is gated while it's not `ready`.
- NewChatBootstrap now derives the status and renders `<Chat>` even on a
  provisioning error (red dot + disabled Send, input still visible)
  instead of swapping in a full-screen error view.

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

* chore(chat): simplify provisioning tooltip copy (#1767)

Shorten the yellow/provisioning workspace-status tooltip to
"Preparing your workspace".

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(chat): drop legacy GET /api/chats/{id}/artist caller (#1767) (#1768)

* fix(chat): drop legacy GET /api/chats/{id}/artist caller (#1767)

* fix(chat): reactive conversations cache + user-scoped session query

* refactor(chat): drop dead /chat/:id artist cache fallback (post-#1765)

The conversations-cache fallback in useArtistFromChat only existed for the
legacy /chat/:id deep link (chatId but no sessionId). That route was
removed in #1765 — every chat is now reached via the canonical
/sessions/{sessionId}/chats/{chatId} URL, so sessionId is always present.

Reduce to the session path: GET /api/sessions/{sessionId} -> session.artistId
-> select. Removes useSyncExternalStore reactive-cache machinery,
findArtistIdInConversationsCache (+ its test), and the unused chatId param.

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

* refactor(chat): validate getSessionById response with zod (PR #1768 review)

Replace type-casting with boundary zod validation, matching the
getConversations convention. Drops the `as GetSessionByIdResponse`/
`as GetSessionByIdErrorResponse` casts in favor of safeParse/parse.

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

* refactor(chat): extract shared safeJsonParse into lib/api (PR #1768 review)

Replace the locally-defined safeJsonParse in getSessionById with a
shared helper in lib/api/, alongside the other HTTP helpers. DRY/SRP.

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

---------

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

* refactor(chat): replace chatId with transportChatId in message editor and hooks

This commit updates the MessageEditor component and the useVercelChat hook to utilize transportChatId instead of the previous chatId. This change enhances the consistency of chat-related API calls and aligns with the new session-scoped architecture, ensuring that the correct chat identifier is used for operations like deleting trailing messages.

* fix(chat): persist selected model before send so the workflow bills it (#1781)

* fix(chat): persist selected model before send so the workflow bills it

The chat-workflow path bills the model it reads from chats.model_id at
request time. The picker selection was never written there, so every new
chat (and any model change) billed the chats.model_id default instead of
the chosen model.

Before sending, await a PATCH of the selected model to
chats.model_id when it changed for the active chat (shouldPersistChatModel
guards redundant writes). The chat row already exists from new-chat
bootstrap, so this covers the first turn race-free without an api change.

Reuses buildPatchChatBody + updateChat(modelId) (originally from #1779).

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

* refactor(chat): extract model-persist-before-send into usePersistSelectedModel

Addresses OCP/SRP review feedback on #1781: instead of inlining the
persist-before-send block in useVercelChat, encapsulate it in a dedicated
usePersistSelectedModel hook (owns the lastPersisted ref + guard + PATCH).
useVercelChat now just calls `await persistSelectedModel()` before send.

Behavior unchanged; pure decision logic still covered by
shouldPersistChatModel tests.

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

* fix(chat): persist selected model on retry/edit + append paths

Addresses review feedback on #1781:
- Retry/Edit call reload() → regenerate (and append() calls sendMessage)
  bypassing handleSubmit, so the selected model was not persisted before
  those sends — the regenerated turn could bill the previous/default model.
  Now await persistSelectedModel() in handleReload and append too.
- Clarify updateChat JSDoc: it patches title and/or modelId (not just rename).

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(chat): repoint chat transport to canonical POST /api/chat (#1783)

Step 3 of the /api/chat/workflow → /api/chat rename (chat#1767), against
the merged docs contract (docs#235) and the api endpoint (api#645, on test).

useChatTransport now POSTs to ${baseUrl}/api/chat instead of
/api/chat/workflow. Comment-only refs in VercelChatProvider updated too.

⚠️ Deploy coordination: requires api#645 (which removes /api/chat/workflow)
to be live in the same environment. Must deploy together with the api
promotion and the open-agents repoint (step 4), or chat 404s.

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

* Revert "refactor(chat): replace chatId with transportChatId in message editor and hooks"

This reverts commit 9f34b11.

* chore: upgrade @privy-io/react-auth v1.88.4 → v3.31.0 (#1802)

* chore: upgrade @privy-io/react-auth v1.88.4 -> v3.31.0

Prerequisite for cross-subdomain (cookie) auth in #1801:
matches marketing's ^3.31.0 so both surfaces run a cookie-capable SDK.

Migration surface is minimal — every @privy-io import in chat is usePrivy
or the PrivyProvider component (audited all 48 sites); none of the
v3-renamed/removed hooks (useLoginToFrame, useSignAuthorization,
useSolanaWallets, useWallets, useLogin/useLogout callbacks) are used, and
usePrivy()'s return shape (login/logout/authenticated/user/getAccessToken/
ready) plus user.email.address / user.wallet.address are unchanged in v3.

Only code change: embeddedWallets.createOnLogin moved under a per-chain key
(v3 breaking change) -> embeddedWallets.ethereum.createOnLogin.

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

* Update providers/PrivyProvider.tsx

---------

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

* feat: show X (Twitter) + LinkedIn in artist Connectors tab (chat#1793) (#1805)

* feat: show X (Twitter) in artist Connectors tab; keep LinkedIn label-only (chat#1793)

Mirror the api ALLOWED_ARTIST_CONNECTORS change on the frontend: add
`twitter` to the artist-settings connector allow-list. `linkedin` stays
excluded (label/owner-only).

TDD: new allowedArtistConnectors.test.ts asserts twitter included +
linkedin excluded, RED before GREEN.

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

* feat: show LinkedIn in artist Connectors tab too (chat#1793)

Reversal of the earlier label-only call: per owner decision 2026-06-18,
LinkedIn is now an artist-facing connector. Add `linkedin` to the chat
ALLOWED_ARTIST_CONNECTORS, mirroring api#680.

TDD: flipped the linkedin assertion (now included), RED before GREEN.

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

* docs: update artist allow-list comment — chat is the source of truth (chat#1793)

The api's ALLOWED_ARTIST_CONNECTORS is being deleted (no runtime consumer;
api#680). Reword the comment to drop the stale "separate from the API's
ALLOWED_ARTIST_CONNECTORS" reference and state that this list is the source
of truth, applied by filtering the unopinionated GET /api/connectors response.

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

* feat: branded icons + display metadata for X, LinkedIn, YouTube connectors (chat#1793)

The artist Connectors tab showed X/LinkedIn (and YouTube) with the generic
Link2 fallback icon, a default description, and a mangled fallback name.
Add proper display entries:
- getConnectorIcon: SiX (X), SiYoutube (YouTube), lucide Linkedin (LinkedIn,
  since Simple Icons dropped the LinkedIn logo on brand request)
- connectorMetadata: descriptions for twitter + linkedin
- formatConnectorName: "X (Twitter)" + "LinkedIn"

TDD: connectorDisplay.test.ts asserts branded (non-fallback) icons + clean
names + real descriptions for the three slugs, RED before GREEN.

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: ahmednahima0-beep <ahmednahima0@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: pradipthaadhi <yulius.upwork@gmail.com>
sweetmantech added a commit that referenced this pull request Jun 29, 2026
* refactor(chat): update chat components to use chatId instead of roomId (#1765)

* refactor(chat): update chat components to use chatId instead of roomId

This commit removes the InstantChatRoom component and updates various hooks and components to replace references of roomId with chatId. The changes ensure that chat functionality is aligned with the new session-scoped architecture, improving consistency across the chat system. Additionally, the sessionId is now required in several components to streamline the chat transport process.

* refactor(chat): integrate sessionId and update chat path utilities

This commit enhances the chat components by introducing the `sessionId` in various hooks and updating the URL handling to utilize new utility functions for generating chat paths. The changes improve the consistency and maintainability of chat navigation, ensuring that the application correctly reflects the session-scoped architecture.

* fix(chat): ensure newRoomId is treated as a string in URL handling

This commit updates the `useCreateArtistTool` hook to explicitly cast `result.newRoomId` to a string when generating the chat path. This change prevents potential type-related issues and ensures consistent URL formatting. Additionally, it adds an import statement for testing utilities in the chat paths test file, enhancing test coverage and maintainability.

* refactor(Header, OrganizationProvider): remove usePathname and replace with window.location.pathname

This commit removes the usePathname hook from both Artist.tsx and OrganizationProvider.tsx, replacing it with direct access to window.location.pathname. This change simplifies the code and maintains the intended navigation behavior without relying on Next.js's router for pathname management.

* refactor(chat): split chatPaths per SRP, drop dead legacy branch, simplify useParams

- Remove dead `/chat/` branch from isActiveChatRoomPath — this PR makes
  /chat/{id} 404, so treating it as an active chat room contradicted the
  PR's own intent and could never fire in production.
- Split lib/chat/chatPaths.ts into one-function-per-file (getChatPath,
  getChatUrl, isActiveChatRoomPath) per repo SRP convention; mirror tests.
- Simplify useVercelChat to `const { chatId } = useParams<{ chatId?: string }>()`
  (KISS), consistent with chat.tsx.

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

---------

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

* feat(chat): defer new-chat bootstrap wait from spinner to send button (#1767) (#1774)

* feat(chat): defer new-chat bootstrap wait from spinner to send button (#1767)

New chats landing on `/` or `/chat` blocked the whole view on a
full-screen spinner while `POST /api/sessions` + `POST /api/sandbox`
resolved (~12s cold, dominated by sandbox provisioning). Render `<Chat>`
immediately with a typeable input instead and provision in parallel; the
Send button stays disabled with a "Preparing your workspace…" cue until
the api-minted `sessionId` + `chatId` land, so the workflow transport
never fires without the ids its validator requires.

- NewChatBootstrap: render <Chat> eagerly with a stable placeholder chat
  id (survives the preparing → ready transition); drop the spinner.
- Thread `sessionId?`, `workflowChatId`, `isBootstrapPreparing` through
  Chat → VercelChatProvider → useVercelChat (provider kept inline).
- useVercelChat: `transportChatId = workflowChatId ?? id` drives the
  transport, optimistic conversation, and URL; only load persisted
  history on a canonical route so no spinner flashes over the input
  during the ready transition.
- ChatInput: gate Send on `isBootstrapPreparing`; input stays typeable.
- useChatTransport / useMessageLoader: sessionId optional + guard.
- useCreateArtistTool: guard the one context sessionId consumer.

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

* fix(chat): send live sessionId/chatId from workflow transport (#1767)

The new-chat flow mounts <Chat> (and useChat) during provisioning with
sessionId undefined + a placeholder chatId. useChat captures the transport
from that mount render and does not swap to the rebuilt instance when the
ids later resolve — so the first send POSTed `sessionId: undefined` to
/api/chat/workflow and got a 400 (the URL rewrote correctly because it
reads the live prop, not the transport).

Read the ids from refs inside transport.body() so a single stable
transport instance always sends the values current as of the request,
regardless of useChat's stale capture. Removed chatId/sessionId from the
memo deps since rebuilding the instance never helped.

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

* feat(chat): replace preparing-text cue with a workspace status dot (#1767)

Swap the flashing "Preparing your workspace…" toolbar text for a
stoplight status dot in the top-right of the chat input: red = off
(provisioning failed / unavailable), yellow = provisioning, green =
ready. Hover shows context via the shadcn tooltip.

- New `WorkspaceStatusIndicator` component in its own file (SRP) — a pure
  display component driven by a `status` prop, built on the existing
  shadcn-based `common/Tooltip`, so it's open for reuse without change.
- Thread `workspaceStatus: "off" | "provisioning" | "ready"` through
  Chat → VercelChatProvider → context (replacing the `isBootstrapPreparing`
  boolean); Send is gated while it's not `ready`.
- NewChatBootstrap now derives the status and renders `<Chat>` even on a
  provisioning error (red dot + disabled Send, input still visible)
  instead of swapping in a full-screen error view.

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

* chore(chat): simplify provisioning tooltip copy (#1767)

Shorten the yellow/provisioning workspace-status tooltip to
"Preparing your workspace".

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(chat): drop legacy GET /api/chats/{id}/artist caller (#1767) (#1768)

* fix(chat): drop legacy GET /api/chats/{id}/artist caller (#1767)

* fix(chat): reactive conversations cache + user-scoped session query

* refactor(chat): drop dead /chat/:id artist cache fallback (post-#1765)

The conversations-cache fallback in useArtistFromChat only existed for the
legacy /chat/:id deep link (chatId but no sessionId). That route was
removed in #1765 — every chat is now reached via the canonical
/sessions/{sessionId}/chats/{chatId} URL, so sessionId is always present.

Reduce to the session path: GET /api/sessions/{sessionId} -> session.artistId
-> select. Removes useSyncExternalStore reactive-cache machinery,
findArtistIdInConversationsCache (+ its test), and the unused chatId param.

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

* refactor(chat): validate getSessionById response with zod (PR #1768 review)

Replace type-casting with boundary zod validation, matching the
getConversations convention. Drops the `as GetSessionByIdResponse`/
`as GetSessionByIdErrorResponse` casts in favor of safeParse/parse.

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

* refactor(chat): extract shared safeJsonParse into lib/api (PR #1768 review)

Replace the locally-defined safeJsonParse in getSessionById with a
shared helper in lib/api/, alongside the other HTTP helpers. DRY/SRP.

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

---------

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

* refactor(chat): replace chatId with transportChatId in message editor and hooks

This commit updates the MessageEditor component and the useVercelChat hook to utilize transportChatId instead of the previous chatId. This change enhances the consistency of chat-related API calls and aligns with the new session-scoped architecture, ensuring that the correct chat identifier is used for operations like deleting trailing messages.

* fix(chat): persist selected model before send so the workflow bills it (#1781)

* fix(chat): persist selected model before send so the workflow bills it

The chat-workflow path bills the model it reads from chats.model_id at
request time. The picker selection was never written there, so every new
chat (and any model change) billed the chats.model_id default instead of
the chosen model.

Before sending, await a PATCH of the selected model to
chats.model_id when it changed for the active chat (shouldPersistChatModel
guards redundant writes). The chat row already exists from new-chat
bootstrap, so this covers the first turn race-free without an api change.

Reuses buildPatchChatBody + updateChat(modelId) (originally from #1779).

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

* refactor(chat): extract model-persist-before-send into usePersistSelectedModel

Addresses OCP/SRP review feedback on #1781: instead of inlining the
persist-before-send block in useVercelChat, encapsulate it in a dedicated
usePersistSelectedModel hook (owns the lastPersisted ref + guard + PATCH).
useVercelChat now just calls `await persistSelectedModel()` before send.

Behavior unchanged; pure decision logic still covered by
shouldPersistChatModel tests.

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

* fix(chat): persist selected model on retry/edit + append paths

Addresses review feedback on #1781:
- Retry/Edit call reload() → regenerate (and append() calls sendMessage)
  bypassing handleSubmit, so the selected model was not persisted before
  those sends — the regenerated turn could bill the previous/default model.
  Now await persistSelectedModel() in handleReload and append too.
- Clarify updateChat JSDoc: it patches title and/or modelId (not just rename).

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(chat): repoint chat transport to canonical POST /api/chat (#1783)

Step 3 of the /api/chat/workflow → /api/chat rename (chat#1767), against
the merged docs contract (docs#235) and the api endpoint (api#645, on test).

useChatTransport now POSTs to ${baseUrl}/api/chat instead of
/api/chat/workflow. Comment-only refs in VercelChatProvider updated too.

⚠️ Deploy coordination: requires api#645 (which removes /api/chat/workflow)
to be live in the same environment. Must deploy together with the api
promotion and the open-agents repoint (step 4), or chat 404s.

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

* Revert "refactor(chat): replace chatId with transportChatId in message editor and hooks"

This reverts commit 9f34b11.

* chore: upgrade @privy-io/react-auth v1.88.4 → v3.31.0 (#1802)

* chore: upgrade @privy-io/react-auth v1.88.4 -> v3.31.0

Prerequisite for cross-subdomain (cookie) auth in #1801:
matches marketing's ^3.31.0 so both surfaces run a cookie-capable SDK.

Migration surface is minimal — every @privy-io import in chat is usePrivy
or the PrivyProvider component (audited all 48 sites); none of the
v3-renamed/removed hooks (useLoginToFrame, useSignAuthorization,
useSolanaWallets, useWallets, useLogin/useLogout callbacks) are used, and
usePrivy()'s return shape (login/logout/authenticated/user/getAccessToken/
ready) plus user.email.address / user.wallet.address are unchanged in v3.

Only code change: embeddedWallets.createOnLogin moved under a per-chain key
(v3 breaking change) -> embeddedWallets.ethereum.createOnLogin.

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

* Update providers/PrivyProvider.tsx

---------

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

* feat: show X (Twitter) + LinkedIn in artist Connectors tab (chat#1793) (#1805)

* feat: show X (Twitter) in artist Connectors tab; keep LinkedIn label-only (chat#1793)

Mirror the api ALLOWED_ARTIST_CONNECTORS change on the frontend: add
`twitter` to the artist-settings connector allow-list. `linkedin` stays
excluded (label/owner-only).

TDD: new allowedArtistConnectors.test.ts asserts twitter included +
linkedin excluded, RED before GREEN.

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

* feat: show LinkedIn in artist Connectors tab too (chat#1793)

Reversal of the earlier label-only call: per owner decision 2026-06-18,
LinkedIn is now an artist-facing connector. Add `linkedin` to the chat
ALLOWED_ARTIST_CONNECTORS, mirroring api#680.

TDD: flipped the linkedin assertion (now included), RED before GREEN.

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

* docs: update artist allow-list comment — chat is the source of truth (chat#1793)

The api's ALLOWED_ARTIST_CONNECTORS is being deleted (no runtime consumer;
api#680). Reword the comment to drop the stale "separate from the API's
ALLOWED_ARTIST_CONNECTORS" reference and state that this list is the source
of truth, applied by filtering the unopinionated GET /api/connectors response.

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

* feat: branded icons + display metadata for X, LinkedIn, YouTube connectors (chat#1793)

The artist Connectors tab showed X/LinkedIn (and YouTube) with the generic
Link2 fallback icon, a default description, and a mangled fallback name.
Add proper display entries:
- getConnectorIcon: SiX (X), SiYoutube (YouTube), lucide Linkedin (LinkedIn,
  since Simple Icons dropped the LinkedIn logo on brand request)
- connectorMetadata: descriptions for twitter + linkedin
- formatConnectorName: "X (Twitter)" + "LinkedIn"

TDD: connectorDisplay.test.ts asserts branded (non-fallback) icons + clean
names + real descriptions for the three slugs, RED before GREEN.

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: point prod API base to api.recoupable.dev (domain cutover) (#1820)

NEW_API_BASE_URL resolved to https://api.recoupable.com in production, which no
longer resolves after the .com apex was retired — every client API call from
chat.recoupable.dev failed with ERR_NAME_NOT_RESOLVED (e.g. /api/ai/models, so
the model selector never populated). Point prod at the live api.recoupable.dev
domain. Non-prod base (test-recoup-api.vercel.app) is unchanged.

Refs #1819.

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

---------

Co-authored-by: ahmednahima0-beep <ahmednahima0@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: pradipthaadhi <yulius.upwork@gmail.com>
sweetmantech added a commit that referenced this pull request Jun 29, 2026
* refactor(chat): update chat components to use chatId instead of roomId (#1765)

* refactor(chat): update chat components to use chatId instead of roomId

This commit removes the InstantChatRoom component and updates various hooks and components to replace references of roomId with chatId. The changes ensure that chat functionality is aligned with the new session-scoped architecture, improving consistency across the chat system. Additionally, the sessionId is now required in several components to streamline the chat transport process.

* refactor(chat): integrate sessionId and update chat path utilities

This commit enhances the chat components by introducing the `sessionId` in various hooks and updating the URL handling to utilize new utility functions for generating chat paths. The changes improve the consistency and maintainability of chat navigation, ensuring that the application correctly reflects the session-scoped architecture.

* fix(chat): ensure newRoomId is treated as a string in URL handling

This commit updates the `useCreateArtistTool` hook to explicitly cast `result.newRoomId` to a string when generating the chat path. This change prevents potential type-related issues and ensures consistent URL formatting. Additionally, it adds an import statement for testing utilities in the chat paths test file, enhancing test coverage and maintainability.

* refactor(Header, OrganizationProvider): remove usePathname and replace with window.location.pathname

This commit removes the usePathname hook from both Artist.tsx and OrganizationProvider.tsx, replacing it with direct access to window.location.pathname. This change simplifies the code and maintains the intended navigation behavior without relying on Next.js's router for pathname management.

* refactor(chat): split chatPaths per SRP, drop dead legacy branch, simplify useParams

- Remove dead `/chat/` branch from isActiveChatRoomPath — this PR makes
  /chat/{id} 404, so treating it as an active chat room contradicted the
  PR's own intent and could never fire in production.
- Split lib/chat/chatPaths.ts into one-function-per-file (getChatPath,
  getChatUrl, isActiveChatRoomPath) per repo SRP convention; mirror tests.
- Simplify useVercelChat to `const { chatId } = useParams<{ chatId?: string }>()`
  (KISS), consistent with chat.tsx.

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

---------

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

* feat(chat): defer new-chat bootstrap wait from spinner to send button (#1767) (#1774)

* feat(chat): defer new-chat bootstrap wait from spinner to send button (#1767)

New chats landing on `/` or `/chat` blocked the whole view on a
full-screen spinner while `POST /api/sessions` + `POST /api/sandbox`
resolved (~12s cold, dominated by sandbox provisioning). Render `<Chat>`
immediately with a typeable input instead and provision in parallel; the
Send button stays disabled with a "Preparing your workspace…" cue until
the api-minted `sessionId` + `chatId` land, so the workflow transport
never fires without the ids its validator requires.

- NewChatBootstrap: render <Chat> eagerly with a stable placeholder chat
  id (survives the preparing → ready transition); drop the spinner.
- Thread `sessionId?`, `workflowChatId`, `isBootstrapPreparing` through
  Chat → VercelChatProvider → useVercelChat (provider kept inline).
- useVercelChat: `transportChatId = workflowChatId ?? id` drives the
  transport, optimistic conversation, and URL; only load persisted
  history on a canonical route so no spinner flashes over the input
  during the ready transition.
- ChatInput: gate Send on `isBootstrapPreparing`; input stays typeable.
- useChatTransport / useMessageLoader: sessionId optional + guard.
- useCreateArtistTool: guard the one context sessionId consumer.

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

* fix(chat): send live sessionId/chatId from workflow transport (#1767)

The new-chat flow mounts <Chat> (and useChat) during provisioning with
sessionId undefined + a placeholder chatId. useChat captures the transport
from that mount render and does not swap to the rebuilt instance when the
ids later resolve — so the first send POSTed `sessionId: undefined` to
/api/chat/workflow and got a 400 (the URL rewrote correctly because it
reads the live prop, not the transport).

Read the ids from refs inside transport.body() so a single stable
transport instance always sends the values current as of the request,
regardless of useChat's stale capture. Removed chatId/sessionId from the
memo deps since rebuilding the instance never helped.

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

* feat(chat): replace preparing-text cue with a workspace status dot (#1767)

Swap the flashing "Preparing your workspace…" toolbar text for a
stoplight status dot in the top-right of the chat input: red = off
(provisioning failed / unavailable), yellow = provisioning, green =
ready. Hover shows context via the shadcn tooltip.

- New `WorkspaceStatusIndicator` component in its own file (SRP) — a pure
  display component driven by a `status` prop, built on the existing
  shadcn-based `common/Tooltip`, so it's open for reuse without change.
- Thread `workspaceStatus: "off" | "provisioning" | "ready"` through
  Chat → VercelChatProvider → context (replacing the `isBootstrapPreparing`
  boolean); Send is gated while it's not `ready`.
- NewChatBootstrap now derives the status and renders `<Chat>` even on a
  provisioning error (red dot + disabled Send, input still visible)
  instead of swapping in a full-screen error view.

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

* chore(chat): simplify provisioning tooltip copy (#1767)

Shorten the yellow/provisioning workspace-status tooltip to
"Preparing your workspace".

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(chat): drop legacy GET /api/chats/{id}/artist caller (#1767) (#1768)

* fix(chat): drop legacy GET /api/chats/{id}/artist caller (#1767)

* fix(chat): reactive conversations cache + user-scoped session query

* refactor(chat): drop dead /chat/:id artist cache fallback (post-#1765)

The conversations-cache fallback in useArtistFromChat only existed for the
legacy /chat/:id deep link (chatId but no sessionId). That route was
removed in #1765 — every chat is now reached via the canonical
/sessions/{sessionId}/chats/{chatId} URL, so sessionId is always present.

Reduce to the session path: GET /api/sessions/{sessionId} -> session.artistId
-> select. Removes useSyncExternalStore reactive-cache machinery,
findArtistIdInConversationsCache (+ its test), and the unused chatId param.

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

* refactor(chat): validate getSessionById response with zod (PR #1768 review)

Replace type-casting with boundary zod validation, matching the
getConversations convention. Drops the `as GetSessionByIdResponse`/
`as GetSessionByIdErrorResponse` casts in favor of safeParse/parse.

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

* refactor(chat): extract shared safeJsonParse into lib/api (PR #1768 review)

Replace the locally-defined safeJsonParse in getSessionById with a
shared helper in lib/api/, alongside the other HTTP helpers. DRY/SRP.

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

---------

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

* refactor(chat): replace chatId with transportChatId in message editor and hooks

This commit updates the MessageEditor component and the useVercelChat hook to utilize transportChatId instead of the previous chatId. This change enhances the consistency of chat-related API calls and aligns with the new session-scoped architecture, ensuring that the correct chat identifier is used for operations like deleting trailing messages.

* fix(chat): persist selected model before send so the workflow bills it (#1781)

* fix(chat): persist selected model before send so the workflow bills it

The chat-workflow path bills the model it reads from chats.model_id at
request time. The picker selection was never written there, so every new
chat (and any model change) billed the chats.model_id default instead of
the chosen model.

Before sending, await a PATCH of the selected model to
chats.model_id when it changed for the active chat (shouldPersistChatModel
guards redundant writes). The chat row already exists from new-chat
bootstrap, so this covers the first turn race-free without an api change.

Reuses buildPatchChatBody + updateChat(modelId) (originally from #1779).

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

* refactor(chat): extract model-persist-before-send into usePersistSelectedModel

Addresses OCP/SRP review feedback on #1781: instead of inlining the
persist-before-send block in useVercelChat, encapsulate it in a dedicated
usePersistSelectedModel hook (owns the lastPersisted ref + guard + PATCH).
useVercelChat now just calls `await persistSelectedModel()` before send.

Behavior unchanged; pure decision logic still covered by
shouldPersistChatModel tests.

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

* fix(chat): persist selected model on retry/edit + append paths

Addresses review feedback on #1781:
- Retry/Edit call reload() → regenerate (and append() calls sendMessage)
  bypassing handleSubmit, so the selected model was not persisted before
  those sends — the regenerated turn could bill the previous/default model.
  Now await persistSelectedModel() in handleReload and append too.
- Clarify updateChat JSDoc: it patches title and/or modelId (not just rename).

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(chat): repoint chat transport to canonical POST /api/chat (#1783)

Step 3 of the /api/chat/workflow → /api/chat rename (chat#1767), against
the merged docs contract (docs#235) and the api endpoint (api#645, on test).

useChatTransport now POSTs to ${baseUrl}/api/chat instead of
/api/chat/workflow. Comment-only refs in VercelChatProvider updated too.

⚠️ Deploy coordination: requires api#645 (which removes /api/chat/workflow)
to be live in the same environment. Must deploy together with the api
promotion and the open-agents repoint (step 4), or chat 404s.

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

* Revert "refactor(chat): replace chatId with transportChatId in message editor and hooks"

This reverts commit 9f34b11.

* chore: upgrade @privy-io/react-auth v1.88.4 → v3.31.0 (#1802)

* chore: upgrade @privy-io/react-auth v1.88.4 -> v3.31.0

Prerequisite for cross-subdomain (cookie) auth in #1801:
matches marketing's ^3.31.0 so both surfaces run a cookie-capable SDK.

Migration surface is minimal — every @privy-io import in chat is usePrivy
or the PrivyProvider component (audited all 48 sites); none of the
v3-renamed/removed hooks (useLoginToFrame, useSignAuthorization,
useSolanaWallets, useWallets, useLogin/useLogout callbacks) are used, and
usePrivy()'s return shape (login/logout/authenticated/user/getAccessToken/
ready) plus user.email.address / user.wallet.address are unchanged in v3.

Only code change: embeddedWallets.createOnLogin moved under a per-chain key
(v3 breaking change) -> embeddedWallets.ethereum.createOnLogin.

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

* Update providers/PrivyProvider.tsx

---------

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

* feat: show X (Twitter) + LinkedIn in artist Connectors tab (chat#1793) (#1805)

* feat: show X (Twitter) in artist Connectors tab; keep LinkedIn label-only (chat#1793)

Mirror the api ALLOWED_ARTIST_CONNECTORS change on the frontend: add
`twitter` to the artist-settings connector allow-list. `linkedin` stays
excluded (label/owner-only).

TDD: new allowedArtistConnectors.test.ts asserts twitter included +
linkedin excluded, RED before GREEN.

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

* feat: show LinkedIn in artist Connectors tab too (chat#1793)

Reversal of the earlier label-only call: per owner decision 2026-06-18,
LinkedIn is now an artist-facing connector. Add `linkedin` to the chat
ALLOWED_ARTIST_CONNECTORS, mirroring api#680.

TDD: flipped the linkedin assertion (now included), RED before GREEN.

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

* docs: update artist allow-list comment — chat is the source of truth (chat#1793)

The api's ALLOWED_ARTIST_CONNECTORS is being deleted (no runtime consumer;
api#680). Reword the comment to drop the stale "separate from the API's
ALLOWED_ARTIST_CONNECTORS" reference and state that this list is the source
of truth, applied by filtering the unopinionated GET /api/connectors response.

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

* feat: branded icons + display metadata for X, LinkedIn, YouTube connectors (chat#1793)

The artist Connectors tab showed X/LinkedIn (and YouTube) with the generic
Link2 fallback icon, a default description, and a mangled fallback name.
Add proper display entries:
- getConnectorIcon: SiX (X), SiYoutube (YouTube), lucide Linkedin (LinkedIn,
  since Simple Icons dropped the LinkedIn logo on brand request)
- connectorMetadata: descriptions for twitter + linkedin
- formatConnectorName: "X (Twitter)" + "LinkedIn"

TDD: connectorDisplay.test.ts asserts branded (non-fallback) icons + clean
names + real descriptions for the three slugs, RED before GREEN.

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: point prod API base to api.recoupable.dev (domain cutover) (#1820)

NEW_API_BASE_URL resolved to https://api.recoupable.com in production, which no
longer resolves after the .com apex was retired — every client API call from
chat.recoupable.dev failed with ERR_NAME_NOT_RESOLVED (e.g. /api/ai/models, so
the model selector never populated). Point prod at the live api.recoupable.dev
domain. Non-prod base (test-recoup-api.vercel.app) is unchanged.

Refs #1819.

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

* fix: repoint dead .com hosts to live .dev (#1819 §A+§B) (#1823)

Repoint dead recoupable.com hosts to their live .dev equivalents and DRY
the URLs through shared constants in lib/consts.ts.

- Add APP_BASE_URL, DOCS_BASE_URL, API_PUBLIC_BASE_URL constants
- getChatUrl: chat.recoupable.com -> APP_BASE_URL (chat.recoupable.dev)
- ApiKeyManager / ExternalLinksGroup docs links -> DOCS_BASE_URL
  (docs.recoupable.dev); apex recoupable.com link left intact (still live)
- In-app API doc examples (account/fans/posts): api.recoupable.com ->
  api.recoupable.dev, via API_PUBLIC_BASE_URL where interpolatable
- JSDoc @see hosts -> docs.recoupable.dev
- Update getChatUrl test assertion to match new origin

Emails (agent@recoupable.com) and apex links left untouched.

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

---------

Co-authored-by: ahmednahima0-beep <ahmednahima0@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: pradipthaadhi <yulius.upwork@gmail.com>
sweetmantech added a commit that referenced this pull request Jun 30, 2026
* refactor(chat): update chat components to use chatId instead of roomId (#1765)

* refactor(chat): update chat components to use chatId instead of roomId

This commit removes the InstantChatRoom component and updates various hooks and components to replace references of roomId with chatId. The changes ensure that chat functionality is aligned with the new session-scoped architecture, improving consistency across the chat system. Additionally, the sessionId is now required in several components to streamline the chat transport process.

* refactor(chat): integrate sessionId and update chat path utilities

This commit enhances the chat components by introducing the `sessionId` in various hooks and updating the URL handling to utilize new utility functions for generating chat paths. The changes improve the consistency and maintainability of chat navigation, ensuring that the application correctly reflects the session-scoped architecture.

* fix(chat): ensure newRoomId is treated as a string in URL handling

This commit updates the `useCreateArtistTool` hook to explicitly cast `result.newRoomId` to a string when generating the chat path. This change prevents potential type-related issues and ensures consistent URL formatting. Additionally, it adds an import statement for testing utilities in the chat paths test file, enhancing test coverage and maintainability.

* refactor(Header, OrganizationProvider): remove usePathname and replace with window.location.pathname

This commit removes the usePathname hook from both Artist.tsx and OrganizationProvider.tsx, replacing it with direct access to window.location.pathname. This change simplifies the code and maintains the intended navigation behavior without relying on Next.js's router for pathname management.

* refactor(chat): split chatPaths per SRP, drop dead legacy branch, simplify useParams

- Remove dead `/chat/` branch from isActiveChatRoomPath — this PR makes
  /chat/{id} 404, so treating it as an active chat room contradicted the
  PR's own intent and could never fire in production.
- Split lib/chat/chatPaths.ts into one-function-per-file (getChatPath,
  getChatUrl, isActiveChatRoomPath) per repo SRP convention; mirror tests.
- Simplify useVercelChat to `const { chatId } = useParams<{ chatId?: string }>()`
  (KISS), consistent with chat.tsx.

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

---------

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

* feat(chat): defer new-chat bootstrap wait from spinner to send button (#1767) (#1774)

* feat(chat): defer new-chat bootstrap wait from spinner to send button (#1767)

New chats landing on `/` or `/chat` blocked the whole view on a
full-screen spinner while `POST /api/sessions` + `POST /api/sandbox`
resolved (~12s cold, dominated by sandbox provisioning). Render `<Chat>`
immediately with a typeable input instead and provision in parallel; the
Send button stays disabled with a "Preparing your workspace…" cue until
the api-minted `sessionId` + `chatId` land, so the workflow transport
never fires without the ids its validator requires.

- NewChatBootstrap: render <Chat> eagerly with a stable placeholder chat
  id (survives the preparing → ready transition); drop the spinner.
- Thread `sessionId?`, `workflowChatId`, `isBootstrapPreparing` through
  Chat → VercelChatProvider → useVercelChat (provider kept inline).
- useVercelChat: `transportChatId = workflowChatId ?? id` drives the
  transport, optimistic conversation, and URL; only load persisted
  history on a canonical route so no spinner flashes over the input
  during the ready transition.
- ChatInput: gate Send on `isBootstrapPreparing`; input stays typeable.
- useChatTransport / useMessageLoader: sessionId optional + guard.
- useCreateArtistTool: guard the one context sessionId consumer.

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

* fix(chat): send live sessionId/chatId from workflow transport (#1767)

The new-chat flow mounts <Chat> (and useChat) during provisioning with
sessionId undefined + a placeholder chatId. useChat captures the transport
from that mount render and does not swap to the rebuilt instance when the
ids later resolve — so the first send POSTed `sessionId: undefined` to
/api/chat/workflow and got a 400 (the URL rewrote correctly because it
reads the live prop, not the transport).

Read the ids from refs inside transport.body() so a single stable
transport instance always sends the values current as of the request,
regardless of useChat's stale capture. Removed chatId/sessionId from the
memo deps since rebuilding the instance never helped.

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

* feat(chat): replace preparing-text cue with a workspace status dot (#1767)

Swap the flashing "Preparing your workspace…" toolbar text for a
stoplight status dot in the top-right of the chat input: red = off
(provisioning failed / unavailable), yellow = provisioning, green =
ready. Hover shows context via the shadcn tooltip.

- New `WorkspaceStatusIndicator` component in its own file (SRP) — a pure
  display component driven by a `status` prop, built on the existing
  shadcn-based `common/Tooltip`, so it's open for reuse without change.
- Thread `workspaceStatus: "off" | "provisioning" | "ready"` through
  Chat → VercelChatProvider → context (replacing the `isBootstrapPreparing`
  boolean); Send is gated while it's not `ready`.
- NewChatBootstrap now derives the status and renders `<Chat>` even on a
  provisioning error (red dot + disabled Send, input still visible)
  instead of swapping in a full-screen error view.

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

* chore(chat): simplify provisioning tooltip copy (#1767)

Shorten the yellow/provisioning workspace-status tooltip to
"Preparing your workspace".

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(chat): drop legacy GET /api/chats/{id}/artist caller (#1767) (#1768)

* fix(chat): drop legacy GET /api/chats/{id}/artist caller (#1767)

* fix(chat): reactive conversations cache + user-scoped session query

* refactor(chat): drop dead /chat/:id artist cache fallback (post-#1765)

The conversations-cache fallback in useArtistFromChat only existed for the
legacy /chat/:id deep link (chatId but no sessionId). That route was
removed in #1765 — every chat is now reached via the canonical
/sessions/{sessionId}/chats/{chatId} URL, so sessionId is always present.

Reduce to the session path: GET /api/sessions/{sessionId} -> session.artistId
-> select. Removes useSyncExternalStore reactive-cache machinery,
findArtistIdInConversationsCache (+ its test), and the unused chatId param.

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

* refactor(chat): validate getSessionById response with zod (PR #1768 review)

Replace type-casting with boundary zod validation, matching the
getConversations convention. Drops the `as GetSessionByIdResponse`/
`as GetSessionByIdErrorResponse` casts in favor of safeParse/parse.

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

* refactor(chat): extract shared safeJsonParse into lib/api (PR #1768 review)

Replace the locally-defined safeJsonParse in getSessionById with a
shared helper in lib/api/, alongside the other HTTP helpers. DRY/SRP.

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

---------

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

* refactor(chat): replace chatId with transportChatId in message editor and hooks

This commit updates the MessageEditor component and the useVercelChat hook to utilize transportChatId instead of the previous chatId. This change enhances the consistency of chat-related API calls and aligns with the new session-scoped architecture, ensuring that the correct chat identifier is used for operations like deleting trailing messages.

* fix(chat): persist selected model before send so the workflow bills it (#1781)

* fix(chat): persist selected model before send so the workflow bills it

The chat-workflow path bills the model it reads from chats.model_id at
request time. The picker selection was never written there, so every new
chat (and any model change) billed the chats.model_id default instead of
the chosen model.

Before sending, await a PATCH of the selected model to
chats.model_id when it changed for the active chat (shouldPersistChatModel
guards redundant writes). The chat row already exists from new-chat
bootstrap, so this covers the first turn race-free without an api change.

Reuses buildPatchChatBody + updateChat(modelId) (originally from #1779).

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

* refactor(chat): extract model-persist-before-send into usePersistSelectedModel

Addresses OCP/SRP review feedback on #1781: instead of inlining the
persist-before-send block in useVercelChat, encapsulate it in a dedicated
usePersistSelectedModel hook (owns the lastPersisted ref + guard + PATCH).
useVercelChat now just calls `await persistSelectedModel()` before send.

Behavior unchanged; pure decision logic still covered by
shouldPersistChatModel tests.

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

* fix(chat): persist selected model on retry/edit + append paths

Addresses review feedback on #1781:
- Retry/Edit call reload() → regenerate (and append() calls sendMessage)
  bypassing handleSubmit, so the selected model was not persisted before
  those sends — the regenerated turn could bill the previous/default model.
  Now await persistSelectedModel() in handleReload and append too.
- Clarify updateChat JSDoc: it patches title and/or modelId (not just rename).

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(chat): repoint chat transport to canonical POST /api/chat (#1783)

Step 3 of the /api/chat/workflow → /api/chat rename (chat#1767), against
the merged docs contract (docs#235) and the api endpoint (api#645, on test).

useChatTransport now POSTs to ${baseUrl}/api/chat instead of
/api/chat/workflow. Comment-only refs in VercelChatProvider updated too.

⚠️ Deploy coordination: requires api#645 (which removes /api/chat/workflow)
to be live in the same environment. Must deploy together with the api
promotion and the open-agents repoint (step 4), or chat 404s.

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

* Revert "refactor(chat): replace chatId with transportChatId in message editor and hooks"

This reverts commit 9f34b11.

* chore: upgrade @privy-io/react-auth v1.88.4 → v3.31.0 (#1802)

* chore: upgrade @privy-io/react-auth v1.88.4 -> v3.31.0

Prerequisite for cross-subdomain (cookie) auth in #1801:
matches marketing's ^3.31.0 so both surfaces run a cookie-capable SDK.

Migration surface is minimal — every @privy-io import in chat is usePrivy
or the PrivyProvider component (audited all 48 sites); none of the
v3-renamed/removed hooks (useLoginToFrame, useSignAuthorization,
useSolanaWallets, useWallets, useLogin/useLogout callbacks) are used, and
usePrivy()'s return shape (login/logout/authenticated/user/getAccessToken/
ready) plus user.email.address / user.wallet.address are unchanged in v3.

Only code change: embeddedWallets.createOnLogin moved under a per-chain key
(v3 breaking change) -> embeddedWallets.ethereum.createOnLogin.

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

* Update providers/PrivyProvider.tsx

---------

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

* feat: show X (Twitter) + LinkedIn in artist Connectors tab (chat#1793) (#1805)

* feat: show X (Twitter) in artist Connectors tab; keep LinkedIn label-only (chat#1793)

Mirror the api ALLOWED_ARTIST_CONNECTORS change on the frontend: add
`twitter` to the artist-settings connector allow-list. `linkedin` stays
excluded (label/owner-only).

TDD: new allowedArtistConnectors.test.ts asserts twitter included +
linkedin excluded, RED before GREEN.

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

* feat: show LinkedIn in artist Connectors tab too (chat#1793)

Reversal of the earlier label-only call: per owner decision 2026-06-18,
LinkedIn is now an artist-facing connector. Add `linkedin` to the chat
ALLOWED_ARTIST_CONNECTORS, mirroring api#680.

TDD: flipped the linkedin assertion (now included), RED before GREEN.

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

* docs: update artist allow-list comment — chat is the source of truth (chat#1793)

The api's ALLOWED_ARTIST_CONNECTORS is being deleted (no runtime consumer;
api#680). Reword the comment to drop the stale "separate from the API's
ALLOWED_ARTIST_CONNECTORS" reference and state that this list is the source
of truth, applied by filtering the unopinionated GET /api/connectors response.

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

* feat: branded icons + display metadata for X, LinkedIn, YouTube connectors (chat#1793)

The artist Connectors tab showed X/LinkedIn (and YouTube) with the generic
Link2 fallback icon, a default description, and a mangled fallback name.
Add proper display entries:
- getConnectorIcon: SiX (X), SiYoutube (YouTube), lucide Linkedin (LinkedIn,
  since Simple Icons dropped the LinkedIn logo on brand request)
- connectorMetadata: descriptions for twitter + linkedin
- formatConnectorName: "X (Twitter)" + "LinkedIn"

TDD: connectorDisplay.test.ts asserts branded (non-fallback) icons + clean
names + real descriptions for the three slugs, RED before GREEN.

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: point prod API base to api.recoupable.dev (domain cutover) (#1820)

NEW_API_BASE_URL resolved to https://api.recoupable.com in production, which no
longer resolves after the .com apex was retired — every client API call from
chat.recoupable.dev failed with ERR_NAME_NOT_RESOLVED (e.g. /api/ai/models, so
the model selector never populated). Point prod at the live api.recoupable.dev
domain. Non-prod base (test-recoup-api.vercel.app) is unchanged.

Refs #1819.

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

* fix: repoint dead .com hosts to live .dev (#1819 §A+§B) (#1823)

Repoint dead recoupable.com hosts to their live .dev equivalents and DRY
the URLs through shared constants in lib/consts.ts.

- Add APP_BASE_URL, DOCS_BASE_URL, API_PUBLIC_BASE_URL constants
- getChatUrl: chat.recoupable.com -> APP_BASE_URL (chat.recoupable.dev)
- ApiKeyManager / ExternalLinksGroup docs links -> DOCS_BASE_URL
  (docs.recoupable.dev); apex recoupable.com link left intact (still live)
- In-app API doc examples (account/fans/posts): api.recoupable.com ->
  api.recoupable.dev, via API_PUBLIC_BASE_URL where interpolatable
- JSDoc @see hosts -> docs.recoupable.dev
- Update getChatUrl test assertion to match new origin

Emails (agent@recoupable.com) and apex links left untouched.

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

* HOLD MERGE: move brand email to @recoupable.dev (#1826)

RECOUP_FROM_EMAIL agent@recoupable.com → @recoupable.dev (the agent's outbound
from-address); global-error page "Get help" support mailto → @recoupable.dev.

DO NOT MERGE until recoupable.dev is a verified sending domain in Resend —
the agent sends from this address, so an unverified domain bounces every email.
Per #1819 decision 2026-06-29.

Refs #1819.

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

---------

Co-authored-by: ahmednahima0-beep <ahmednahima0@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: pradipthaadhi <yulius.upwork@gmail.com>
sweetmantech added a commit that referenced this pull request Jun 30, 2026
* refactor(chat): update chat components to use chatId instead of roomId (#1765)

* refactor(chat): update chat components to use chatId instead of roomId

This commit removes the InstantChatRoom component and updates various hooks and components to replace references of roomId with chatId. The changes ensure that chat functionality is aligned with the new session-scoped architecture, improving consistency across the chat system. Additionally, the sessionId is now required in several components to streamline the chat transport process.

* refactor(chat): integrate sessionId and update chat path utilities

This commit enhances the chat components by introducing the `sessionId` in various hooks and updating the URL handling to utilize new utility functions for generating chat paths. The changes improve the consistency and maintainability of chat navigation, ensuring that the application correctly reflects the session-scoped architecture.

* fix(chat): ensure newRoomId is treated as a string in URL handling

This commit updates the `useCreateArtistTool` hook to explicitly cast `result.newRoomId` to a string when generating the chat path. This change prevents potential type-related issues and ensures consistent URL formatting. Additionally, it adds an import statement for testing utilities in the chat paths test file, enhancing test coverage and maintainability.

* refactor(Header, OrganizationProvider): remove usePathname and replace with window.location.pathname

This commit removes the usePathname hook from both Artist.tsx and OrganizationProvider.tsx, replacing it with direct access to window.location.pathname. This change simplifies the code and maintains the intended navigation behavior without relying on Next.js's router for pathname management.

* refactor(chat): split chatPaths per SRP, drop dead legacy branch, simplify useParams

- Remove dead `/chat/` branch from isActiveChatRoomPath — this PR makes
  /chat/{id} 404, so treating it as an active chat room contradicted the
  PR's own intent and could never fire in production.
- Split lib/chat/chatPaths.ts into one-function-per-file (getChatPath,
  getChatUrl, isActiveChatRoomPath) per repo SRP convention; mirror tests.
- Simplify useVercelChat to `const { chatId } = useParams<{ chatId?: string }>()`
  (KISS), consistent with chat.tsx.

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

---------

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

* feat(chat): defer new-chat bootstrap wait from spinner to send button (#1767) (#1774)

* feat(chat): defer new-chat bootstrap wait from spinner to send button (#1767)

New chats landing on `/` or `/chat` blocked the whole view on a
full-screen spinner while `POST /api/sessions` + `POST /api/sandbox`
resolved (~12s cold, dominated by sandbox provisioning). Render `<Chat>`
immediately with a typeable input instead and provision in parallel; the
Send button stays disabled with a "Preparing your workspace…" cue until
the api-minted `sessionId` + `chatId` land, so the workflow transport
never fires without the ids its validator requires.

- NewChatBootstrap: render <Chat> eagerly with a stable placeholder chat
  id (survives the preparing → ready transition); drop the spinner.
- Thread `sessionId?`, `workflowChatId`, `isBootstrapPreparing` through
  Chat → VercelChatProvider → useVercelChat (provider kept inline).
- useVercelChat: `transportChatId = workflowChatId ?? id` drives the
  transport, optimistic conversation, and URL; only load persisted
  history on a canonical route so no spinner flashes over the input
  during the ready transition.
- ChatInput: gate Send on `isBootstrapPreparing`; input stays typeable.
- useChatTransport / useMessageLoader: sessionId optional + guard.
- useCreateArtistTool: guard the one context sessionId consumer.

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

* fix(chat): send live sessionId/chatId from workflow transport (#1767)

The new-chat flow mounts <Chat> (and useChat) during provisioning with
sessionId undefined + a placeholder chatId. useChat captures the transport
from that mount render and does not swap to the rebuilt instance when the
ids later resolve — so the first send POSTed `sessionId: undefined` to
/api/chat/workflow and got a 400 (the URL rewrote correctly because it
reads the live prop, not the transport).

Read the ids from refs inside transport.body() so a single stable
transport instance always sends the values current as of the request,
regardless of useChat's stale capture. Removed chatId/sessionId from the
memo deps since rebuilding the instance never helped.

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

* feat(chat): replace preparing-text cue with a workspace status dot (#1767)

Swap the flashing "Preparing your workspace…" toolbar text for a
stoplight status dot in the top-right of the chat input: red = off
(provisioning failed / unavailable), yellow = provisioning, green =
ready. Hover shows context via the shadcn tooltip.

- New `WorkspaceStatusIndicator` component in its own file (SRP) — a pure
  display component driven by a `status` prop, built on the existing
  shadcn-based `common/Tooltip`, so it's open for reuse without change.
- Thread `workspaceStatus: "off" | "provisioning" | "ready"` through
  Chat → VercelChatProvider → context (replacing the `isBootstrapPreparing`
  boolean); Send is gated while it's not `ready`.
- NewChatBootstrap now derives the status and renders `<Chat>` even on a
  provisioning error (red dot + disabled Send, input still visible)
  instead of swapping in a full-screen error view.

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

* chore(chat): simplify provisioning tooltip copy (#1767)

Shorten the yellow/provisioning workspace-status tooltip to
"Preparing your workspace".

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(chat): drop legacy GET /api/chats/{id}/artist caller (#1767) (#1768)

* fix(chat): drop legacy GET /api/chats/{id}/artist caller (#1767)

* fix(chat): reactive conversations cache + user-scoped session query

* refactor(chat): drop dead /chat/:id artist cache fallback (post-#1765)

The conversations-cache fallback in useArtistFromChat only existed for the
legacy /chat/:id deep link (chatId but no sessionId). That route was
removed in #1765 — every chat is now reached via the canonical
/sessions/{sessionId}/chats/{chatId} URL, so sessionId is always present.

Reduce to the session path: GET /api/sessions/{sessionId} -> session.artistId
-> select. Removes useSyncExternalStore reactive-cache machinery,
findArtistIdInConversationsCache (+ its test), and the unused chatId param.

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

* refactor(chat): validate getSessionById response with zod (PR #1768 review)

Replace type-casting with boundary zod validation, matching the
getConversations convention. Drops the `as GetSessionByIdResponse`/
`as GetSessionByIdErrorResponse` casts in favor of safeParse/parse.

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

* refactor(chat): extract shared safeJsonParse into lib/api (PR #1768 review)

Replace the locally-defined safeJsonParse in getSessionById with a
shared helper in lib/api/, alongside the other HTTP helpers. DRY/SRP.

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

---------

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

* refactor(chat): replace chatId with transportChatId in message editor and hooks

This commit updates the MessageEditor component and the useVercelChat hook to utilize transportChatId instead of the previous chatId. This change enhances the consistency of chat-related API calls and aligns with the new session-scoped architecture, ensuring that the correct chat identifier is used for operations like deleting trailing messages.

* fix(chat): persist selected model before send so the workflow bills it (#1781)

* fix(chat): persist selected model before send so the workflow bills it

The chat-workflow path bills the model it reads from chats.model_id at
request time. The picker selection was never written there, so every new
chat (and any model change) billed the chats.model_id default instead of
the chosen model.

Before sending, await a PATCH of the selected model to
chats.model_id when it changed for the active chat (shouldPersistChatModel
guards redundant writes). The chat row already exists from new-chat
bootstrap, so this covers the first turn race-free without an api change.

Reuses buildPatchChatBody + updateChat(modelId) (originally from #1779).

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

* refactor(chat): extract model-persist-before-send into usePersistSelectedModel

Addresses OCP/SRP review feedback on #1781: instead of inlining the
persist-before-send block in useVercelChat, encapsulate it in a dedicated
usePersistSelectedModel hook (owns the lastPersisted ref + guard + PATCH).
useVercelChat now just calls `await persistSelectedModel()` before send.

Behavior unchanged; pure decision logic still covered by
shouldPersistChatModel tests.

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

* fix(chat): persist selected model on retry/edit + append paths

Addresses review feedback on #1781:
- Retry/Edit call reload() → regenerate (and append() calls sendMessage)
  bypassing handleSubmit, so the selected model was not persisted before
  those sends — the regenerated turn could bill the previous/default model.
  Now await persistSelectedModel() in handleReload and append too.
- Clarify updateChat JSDoc: it patches title and/or modelId (not just rename).

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(chat): repoint chat transport to canonical POST /api/chat (#1783)

Step 3 of the /api/chat/workflow → /api/chat rename (chat#1767), against
the merged docs contract (docs#235) and the api endpoint (api#645, on test).

useChatTransport now POSTs to ${baseUrl}/api/chat instead of
/api/chat/workflow. Comment-only refs in VercelChatProvider updated too.

⚠️ Deploy coordination: requires api#645 (which removes /api/chat/workflow)
to be live in the same environment. Must deploy together with the api
promotion and the open-agents repoint (step 4), or chat 404s.

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

* Revert "refactor(chat): replace chatId with transportChatId in message editor and hooks"

This reverts commit 9f34b11.

* chore: upgrade @privy-io/react-auth v1.88.4 → v3.31.0 (#1802)

* chore: upgrade @privy-io/react-auth v1.88.4 -> v3.31.0

Prerequisite for cross-subdomain (cookie) auth in #1801:
matches marketing's ^3.31.0 so both surfaces run a cookie-capable SDK.

Migration surface is minimal — every @privy-io import in chat is usePrivy
or the PrivyProvider component (audited all 48 sites); none of the
v3-renamed/removed hooks (useLoginToFrame, useSignAuthorization,
useSolanaWallets, useWallets, useLogin/useLogout callbacks) are used, and
usePrivy()'s return shape (login/logout/authenticated/user/getAccessToken/
ready) plus user.email.address / user.wallet.address are unchanged in v3.

Only code change: embeddedWallets.createOnLogin moved under a per-chain key
(v3 breaking change) -> embeddedWallets.ethereum.createOnLogin.

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

* Update providers/PrivyProvider.tsx

---------

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

* feat: show X (Twitter) + LinkedIn in artist Connectors tab (chat#1793) (#1805)

* feat: show X (Twitter) in artist Connectors tab; keep LinkedIn label-only (chat#1793)

Mirror the api ALLOWED_ARTIST_CONNECTORS change on the frontend: add
`twitter` to the artist-settings connector allow-list. `linkedin` stays
excluded (label/owner-only).

TDD: new allowedArtistConnectors.test.ts asserts twitter included +
linkedin excluded, RED before GREEN.

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

* feat: show LinkedIn in artist Connectors tab too (chat#1793)

Reversal of the earlier label-only call: per owner decision 2026-06-18,
LinkedIn is now an artist-facing connector. Add `linkedin` to the chat
ALLOWED_ARTIST_CONNECTORS, mirroring api#680.

TDD: flipped the linkedin assertion (now included), RED before GREEN.

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

* docs: update artist allow-list comment — chat is the source of truth (chat#1793)

The api's ALLOWED_ARTIST_CONNECTORS is being deleted (no runtime consumer;
api#680). Reword the comment to drop the stale "separate from the API's
ALLOWED_ARTIST_CONNECTORS" reference and state that this list is the source
of truth, applied by filtering the unopinionated GET /api/connectors response.

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

* feat: branded icons + display metadata for X, LinkedIn, YouTube connectors (chat#1793)

The artist Connectors tab showed X/LinkedIn (and YouTube) with the generic
Link2 fallback icon, a default description, and a mangled fallback name.
Add proper display entries:
- getConnectorIcon: SiX (X), SiYoutube (YouTube), lucide Linkedin (LinkedIn,
  since Simple Icons dropped the LinkedIn logo on brand request)
- connectorMetadata: descriptions for twitter + linkedin
- formatConnectorName: "X (Twitter)" + "LinkedIn"

TDD: connectorDisplay.test.ts asserts branded (non-fallback) icons + clean
names + real descriptions for the three slugs, RED before GREEN.

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: point prod API base to api.recoupable.dev (domain cutover) (#1820)

NEW_API_BASE_URL resolved to https://api.recoupable.com in production, which no
longer resolves after the .com apex was retired — every client API call from
chat.recoupable.dev failed with ERR_NAME_NOT_RESOLVED (e.g. /api/ai/models, so
the model selector never populated). Point prod at the live api.recoupable.dev
domain. Non-prod base (test-recoup-api.vercel.app) is unchanged.

Refs #1819.

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

* fix: repoint dead .com hosts to live .dev (#1819 §A+§B) (#1823)

Repoint dead recoupable.com hosts to their live .dev equivalents and DRY
the URLs through shared constants in lib/consts.ts.

- Add APP_BASE_URL, DOCS_BASE_URL, API_PUBLIC_BASE_URL constants
- getChatUrl: chat.recoupable.com -> APP_BASE_URL (chat.recoupable.dev)
- ApiKeyManager / ExternalLinksGroup docs links -> DOCS_BASE_URL
  (docs.recoupable.dev); apex recoupable.com link left intact (still live)
- In-app API doc examples (account/fans/posts): api.recoupable.com ->
  api.recoupable.dev, via API_PUBLIC_BASE_URL where interpolatable
- JSDoc @see hosts -> docs.recoupable.dev
- Update getChatUrl test assertion to match new origin

Emails (agent@recoupable.com) and apex links left untouched.

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

* HOLD MERGE: move brand email to @recoupable.dev (#1826)

RECOUP_FROM_EMAIL agent@recoupable.com → @recoupable.dev (the agent's outbound
from-address); global-error page "Get help" support mailto → @recoupable.dev.

DO NOT MERGE until recoupable.dev is a verified sending domain in Resend —
the agent sends from this address, so an unverified domain bounces every email.
Per #1819 decision 2026-06-29.

Refs #1819.

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

* fix: move apex recoupable.com → recoupable.dev (chat Website + legal redirects) (#1825)

Update the Website link in the user profile dropdown and the privacy/terms
redirects from recoupable.com to the live recoupable.dev apex.

- ExternalLinksGroup: Website href + label → recoupable.dev
- /privacy → https://recoupable.dev/privacy-policy
- /terms → https://recoupable.dev/terms-of-use (path renamed from terms-of-service)

Refs #1819

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

---------

Co-authored-by: ahmednahima0-beep <ahmednahima0@gmail.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: pradipthaadhi <yulius.upwork@gmail.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.

2 participants