Skip to content

Feat/fork and edit#15

Merged
DIodide merged 3 commits intostagingfrom
feat/fork_and_edit
Mar 18, 2026
Merged

Feat/fork and edit#15
DIodide merged 3 commits intostagingfrom
feat/fork_and_edit

Conversation

@richardw19
Copy link
Collaborator

Main Features Added:

Fork Conversation: Below each chat response message, there is a "Fork" button which allows the user to copy the entire chat conversation up until this point into another conversation. A banner is shown in the forked conversation which can be clicked to link back to the original.

Edit Prompt: Below each user prompt message, there is an "Edit" button which allows the user to edit their previous prompts and subsequently generate new chat response messages. This implicitly creates a new conversation branch and can be thought of as a conversation tree. There is also a pagination feature which allows users to view past versions of their edited prompts.

…nt version histories of an edited prompt, which implicitly forks a new conversation and retains all past histories. Used a rooted tree structure for conversation history management.
@richardw19 richardw19 requested a review from DIodide March 18, 2026 03:50
@DIodide
Copy link
Owner

DIodide commented Mar 18, 2026

@claude review this

@claude
Copy link

claude bot commented Mar 18, 2026

Claude finished @DIodide's task in 2m 8s —— View job


PR Review: Feat/fork and edit

  • Gather context and read changed files
  • Review schema.ts changes
  • Review conversations.ts changes
  • Review message-actions.tsx changes
  • Review chat/index.tsx changes
  • Post final review

Overall this is a solid implementation of a genuinely complex feature. The tree-based conversation model is well-thought-out and the pagination UX is a nice touch. Below are findings roughly ordered by severity.


Bugs / Correctness Issues

1. Race condition in handleSaveEditPrompt — UX gap (chat/index.tsx ~line 160)

const newConvoId = await editForkConversation.mutateAsync(...);
handleSelectConversation(newConvoId);   // ← switches UI immediately

await sendMessageMutation.mutateAsync({  // ← message not in DB yet
  conversationId: newConvoId,
  ...
});

handleSelectConversation fires before the user message exists in the DB. The UI switches to the new (empty) conversation for a render cycle, which will flash a blank chat. Consider sending the message before navigating, or using a pending/optimistic state to suppress the empty-state render.

Fix this →


2. No infinite-loop guard in editFork ancestor walk (conversations.ts ~line 890)

for (;;) {
    if (!current.editParentConversationId) { ... break; }
    ...
    const parent = await ctx.db.get(current.editParentConversationId);
    current = parent;
}

If the DB ever has a cycle in editParentConversationId (e.g., due to a bug or data corruption), this will loop forever inside a Convex mutation. Add a depth counter (e.g., bail out after 50 iterations) as a safety guard.

Fix this →


3. editFork N+1 DB reads (conversations.ts ~line 900)

Each iteration of the ancestor walk issues a ctx.db.get. For a conversation edited N times at the same position, this is N sequential reads inside one mutation. In practice edit trees are shallow, but it's worth documenting this assumption or pre-fetching ancestry in one query.


4. textarea ref callback steals focus on every re-render (chat/index.tsx ~line 580)

ref={(el) => {
    if (el) {
        el.focus();
        el.setSelectionRange(el.value.length, el.value.length);
    }
}}

This runs every time the parent re-renders while the textarea is mounted (e.g., on every keystroke since editingContent state changes). Use useEffect or useRef + useEffect to focus only on mount.

Fix this →


Performance Issues

5. O(conversations × messages) filter inside the render loop (chat/index.tsx ~lines 378–392)

const editSiblings = editRootId !== undefined
    ? allConversations.filter(
        (c) =>
            c.editParentConversationId === editRootId &&
            c.editParentMessageCount === i,
    )
    : [];

This runs for every message on every render. With many messages and many conversations this compounds quickly. Consider building a grouped lookup map (keyed by editParentConversationId + editParentMessageCount) inside the same useMemo that builds convoMap.

Fix this →

6. forkedFromConversationTitle linear scan on every render (chat/index.tsx ~line 241)

conversations?.find((c) => c._id === activeConversation.forkedFromConversationId)?.title

This is computed inline during render. Move it to a useMemo alongside the other conversation lookups.


Code Quality / Maintainability

7. Type cast in sidebar filter (chat/index.tsx ~line 218)

(c) => !(c as Record<string, unknown>).editParentConversationId,

Convex generates typed query results. If the type already includes editParentConversationId?: Id<"conversations">, this cast is unnecessary and hides potential type errors. Use c.editParentConversationId directly.


8. Large prop surface on ChatMessages (chat/index.tsx ~lines 1284–1551)

13 new props were added to ChatMessages (edit state, fork callbacks, pagination data, conversation tree, etc.). This component is becoming a "god component." Consider grouping related props into an object (e.g., editState: { editingMessageId, editingContent, onEditContentChange, onStartEditPrompt, onCancelEditPrompt, onSaveEditPrompt }) or extracting pagination into a custom hook.


9. Missing loading/disabled states during fork/edit operations

handleFork and handleSaveEditPrompt are async but neither the Fork button nor the Save button disables during inflight mutations. Double-clicks could create duplicate conversations. Pass forkConversation.isPending / editForkConversation.isPending through to disable buttons while loading.

Fix this →


10. editFork doesn't propagate forkedFromConversationId (conversations.ts ~line 912)

When editing a prompt inside a conversation that was itself a regular fork, the edit-fork child won't show the "Branched from X" banner because forkedFromConversationId isn't copied. This may be intentional (the fork banner is not part of the edit lineage), but it could confuse users who edit inside a forked conversation and expect the context to be preserved. Worth documenting or handling explicitly.


Minor / Nits

  • Typo in commit message: "pagenation" → "pagination"
  • The pagination arrows ( / ) render as raw HTML entities in some font configs — consider using ChevronLeft/ChevronRight from lucide-react for visual consistency with the rest of the app
  • "Fork of ${convo.title}" as the forked conversation title will nest awkwardly ("Fork of Fork of ...") after multiple forks — consider a smarter naming strategy

Summary

Category Count
Bugs 2 (race condition, focus steal)
Safety 1 (infinite loop guard)
Performance 2
Quality 4

The core tree structure is correct and the feature works end-to-end. The most important fixes before merging are the UX race condition (#1) and the textarea focus re-fire (#4). The infinite loop guard (#2) is a low-probability but high-severity edge case worth addressing.

@DIodide DIodide merged commit a9f7d34 into staging Mar 18, 2026
1 check passed
@DIodide DIodide deleted the feat/fork_and_edit branch March 18, 2026 18:09
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