Skip to content

Conversation

@Junyi-99
Copy link
Member

@Junyi-99 Junyi-99 commented Jan 8, 2026

This pull request introduces significant improvements to the conversation branching system, adds support for new GPT-5 models, and refines how conversations and their branches are managed and returned via the API. The main changes include implementing a robust branching mechanism for chat conversations, updating the API to support branch selection and history, and expanding the list of supported language models.

Conversation Branching and API Enhancements:

  • Added a Branch struct to Conversation in models/conversation.go, enabling multiple branches per conversation for edit history, and migrated legacy fields for backward compatibility.
  • Refactored conversation message handling in create_conversation_message_stream_v2.go to append messages to the correct branch, support branch creation on message edits, and ensure all API methods use the active or specified branch for chat history and LLM calls. [1] [2] [3] [4] [5] [6] [7]
  • Updated the conversation-to-proto mapping in mapper/conversation_v2.go to allow explicit branch selection, include branch metadata in API responses, and ensure backward compatibility with legacy conversations. [1] [2]
  • Modified the GetConversation API to allow branch selection via a branch_id parameter, returning the correct branch's history and metadata.

Supported Models Expansion:

  • Added support for new GPT-5.1, GPT-5.2, and GPT-5.2 Pro models (with corresponding context sizes and pricing) in the supported models API, both for users with and without a custom OpenAI API key. [1] [2]

Other Changes:

  • Commented out two default prompts in the user prompts list, possibly for deprecation or future updates.
  • Cleaned up unused imports in the user prompts handler.

Note

Introduces branch-aware conversations and reasoning streaming, plus new GPT-5.x models.

  • Models: add Branch to Conversation with migration helpers (EnsureBranches, CreateNewBranch, GetActiveBranch); histories now per-branch
  • API/server: message append targets active/new branch; parent_message_id creates a branched edit; LLM calls use branch history; GetConversation accepts branch_id; mapper returns branch metadata (branches, current_branch_id, indices)
  • Protobuf/gateway: extend chat.proto with BranchInfo, branch fields on Conversation, optional branch_id and parent_message_id; add ReasoningChunk; regenerate pb and gw
  • Streaming: capture and stream reasoning deltas; HandleTextDoneItem includes reasoning; in-app assistant messages support reasoning
  • Frontend: branch switcher UI, user message edit -> sends with parentMessageId and truncates UI to parent; reasoning shown above answers; updates to converters/handlers; minor UI tweaks and safer prompt selection
  • Models list: add GPT-5.1, GPT-5.2, GPT-5.2 Pro with contexts/pricing for both key/no-key paths

Written by Cursor Bugbot for commit 98580ce. This will update automatically on new commits. Configure here.

Copilot AI review requested due to automatic review settings January 8, 2026 07:34
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This pull request implements a comprehensive conversation branching system that enables users to edit messages and create alternative conversation paths, along with adding support for new GPT-5 models and reasoning display features. The implementation spans both frontend (React/TypeScript) and backend (Go) with protocol buffer schema updates.

Key changes:

  • Implements conversation branching architecture with Branch struct for tracking edit history
  • Adds GPT-5.1, GPT-5.2, and GPT-5.2 Pro model support with pricing information
  • Introduces reasoning token display for AI responses with streaming support

Reviewed changes

Copilot reviewed 32 out of 32 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
internal/models/conversation.go Adds Branch struct and methods for branch management, migration from legacy format
internal/api/chat/create_conversation_message_stream_v2.go Updates message streaming to use branches, handles parent message IDs for edits
internal/api/mapper/conversation_v2.go Maps conversation model to proto with branch selection support
internal/api/chat/get_conversation_v2.go Supports optional branch_id parameter for retrieving specific branches
internal/api/chat/list_supported_models_v2.go Adds GPT-5.x model definitions with context sizes and pricing
proto/chat/v2/chat.proto Adds BranchInfo, reasoning field, ReasoningChunk, and parent_message_id
pkg/gen/api/chat/v2/*.go Generated Go code from proto changes
webapp/_webapp/src/components/branch-switcher.tsx New UI component for navigating between conversation branches
webapp/_webapp/src/components/message-entry-container/user.tsx Adds message editing interface with edit button and textarea
webapp/_webapp/src/components/message-entry-container/assistant.tsx Displays reasoning content in expandable card format
webapp/_webapp/src/hooks/useSendMessageStream.ts Handles parentMessageId for branching, truncates messages client-side
internal/services/toolkit/handler/stream_v2.go Adds HandleReasoningDelta for streaming reasoning tokens
internal/services/toolkit/client/completion_v2.go Parses reasoning_content and reasoning fields from streaming responses
webapp/_webapp/src/stores/conversation/handlers/handleReasoningChunk.ts New handler for processing reasoning chunks in streaming messages
Multiple handler files Removes "openai_" prefix from message IDs for cleaner identifier format
webapp/_webapp/src/views/settings/*.tsx Reduces label text size from text-sm to text-xs
webapp/_webapp/src/main.tsx Adds URL patterns to Faro monitoring ignore list

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +47 to +61
const handlePrevious = () => {
if (currentIndex <= 1) return;
const prevBranchId = branches[currentIndex - 2]?.id;
if (prevBranchId) {
switchToBranch(prevBranchId);
}
};

const handleNext = () => {
if (currentIndex >= totalBranches) return;
const nextBranchId = branches[currentIndex]?.id;
if (nextBranchId) {
switchToBranch(nextBranchId);
}
};
Copy link

Copilot AI Jan 8, 2026

Choose a reason for hiding this comment

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

The component should handle the case where conversation.branches array length doesn't match conversation.totalBranches. If the arrays are out of sync, accessing branches[currentIndex - 2] or branches[currentIndex] could return undefined even when the navigation should be valid, or conversely allow navigation when it shouldn't be possible.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Copilot AI commented Jan 8, 2026

@Junyi-99 I've opened a new pull request, #80, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

This PR is being reviewed by Cursor Bugbot

Details

You are on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

}
// For new conversations, ensure branches and get the active one
conversation.EnsureBranches()
activeBranch = conversation.GetActiveBranch()
Copy link

Choose a reason for hiding this comment

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

Missing nil check for activeBranch in new conversations

High Severity

For new conversations, GetActiveBranch() is called without a nil check after EnsureBranches(). This is inconsistent with the existing conversation path in appendConversationMessage, which properly checks for nil at lines 175-179. If GetActiveBranch() returns nil (e.g., if EnsureBranches() doesn't create a branch), accessing activeBranch.OpenaiChatHistoryCompletion at line 319 will cause a nil pointer dereference and crash the server.

Additional Locations (1)

Fix in Cursor Fix in Web

const updatedParts = prevMessage.parts.map((part: MessageEntry) => {
const isTargetPart = part.messageId === chunk.messageId && part.assistant;

if (!isTargetPart) return part;
Copy link

Choose a reason for hiding this comment

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

Reasoning chunks silently dropped due to race condition

High Severity

The backend sends ReasoningChunk messages before sending StreamPartBegin for the assistant message. The frontend handler checks part.messageId === chunk.messageId && part.assistant, but the assistant part doesn't exist yet since handleStreamPartBegin hasn't been called. This causes all reasoning content to be silently dropped because isTargetPart evaluates to false, making the new reasoning feature non-functional for streamed responses.

Additional Locations (1)

Fix in Cursor Fix in Web

currentBranchIndex: branchInfo.currentBranchIndex,
totalBranches: branchInfo.totalBranches,
}));
}
Copy link

Choose a reason for hiding this comment

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

Async branch update can corrupt unrelated conversation state

Medium Severity

The updateBranchInfoAsync function fetches branch info for a specific conversationId but applies the update to whatever currentConversation exists when the async call completes. If the user navigates to a different conversation before the fetch completes, the branch info from the old conversation (with currentBranchId, branches, etc.) will overwrite the new conversation's branch metadata. The function needs to verify that currentConversation.id still matches the fetched conversationId before applying the update.

Fix in Cursor Fix in Web

@Junyi-99
Copy link
Member Author

Junyi-99 commented Jan 8, 2026

related to #71

@Junyi-99 Junyi-99 moved this from Icebox to In review in Project PaperDebugger Jan 8, 2026
@Junyi-99 Junyi-99 moved this from In review to QA / Testing in Project PaperDebugger Jan 8, 2026
@Junyi-99 Junyi-99 requested a review from Copilot January 9, 2026 09:01
…ches (#80)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Junyi-99 <14367694+Junyi-99@users.noreply.github.com>
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 32 out of 32 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 333 to 334
}
bsonMessages[i] = bsonMsg
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The branch's UpdatedAt timestamp is not being updated when AI response messages are appended to it. This means GetActiveBranch() will not correctly identify the most recently modified branch. You should update activeBranch.UpdatedAt to the current time before saving the conversation.

Copilot uses AI. Check for mistakes.
// Find parent message and truncate after it
foundIndex := -1
for i, msg := range sourceInappHistory {
if id, ok := msg["messageId"].(string); ok && strings.Contains(id, truncateAfterMsgID) {
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

Using strings.Contains for message ID matching could lead to false positives if one message ID is a substring of another. Consider using exact equality comparison instead: id == truncateAfterMsgID.

Copilot uses AI. Check for mistakes.
Comment on lines 93 to 106
if field, ok := delta.JSON.ExtraFields["reasoning_content"]; ok && field.Raw() != "null" {
var s string
err := json.Unmarshal([]byte(field.Raw()), &s)
if err != nil {
// fmt.Println(err)
if err == nil {
reasoning_content += s
streamHandler.HandleReasoningDelta(chunk.ID, s)
}
} else if field, ok := delta.JSON.ExtraFields["reasoning"]; ok && field.Raw() != "null" {
var s string
err := json.Unmarshal([]byte(field.Raw()), &s)
if err == nil {
reasoning_content += s
streamHandler.HandleReasoningDelta(chunk.ID, s)
}
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The reasoning field handling in the OpenAI response parsing checks for both "reasoning_content" and "reasoning" fields. This dual check suggests uncertainty about the field name. Ensure you're checking the correct field name that the OpenAI API actually uses for reasoning output, as incorrect field names will silently fail to capture reasoning data.

Copilot uses AI. Check for mistakes.
Comment on lines +38 to +44
if (prompts.length === 0) {
return (
<div className="transition-all duration-100 absolute bottom-full left-0 right-0 mb-1 z-50 bg-white shadow-lg rounded-lg border border-gray-200 p-4">
<div className="text-gray-500 text-sm text-center">No prompts found</div>
</div>
);
}
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

The "No prompts found" message is shown when prompts.length === 0, but the parent condition requires prompt.startsWith("/"). This means the "No prompts found" state will only show when the user has typed "/" but no prompts match, not when the prompts list is genuinely empty. Consider whether this is the intended behavior.

Copilot uses AI. Check for mistakes.
Comment on lines +32 to +36
// Use specified branch_id if provided, otherwise use active branch
branchID := req.GetBranchId()

return &chatv2.GetConversationResponse{
Conversation: mapper.MapModelConversationToProtoV2(conversation),
Conversation: mapper.MapModelConversationToProtoV2WithBranch(conversation, branchID),
Copy link

Copilot AI Jan 9, 2026

Choose a reason for hiding this comment

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

Missing input validation for branchId parameter. The code should validate that the provided branchId exists in the conversation's branches before attempting to use it. Currently, if an invalid branchId is provided, the code will silently fall back to the active branch without indicating an error to the client.

Copilot uses AI. Check for mistakes.
messages: [],
}));
}
}
Copy link

Choose a reason for hiding this comment

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

Client truncates messages before API call without rollback

Medium Severity

When editing a message, the client-side conversation is truncated immediately before the streaming API call. If the API call fails or encounters an error, the truncated messages are not restored. The error handlers (handleError, handleStreamError) only mark streaming messages as stale but do not restore the previously truncated conversation messages. This leaves the client in an inconsistent state where it shows fewer messages than what the server has stored.

Fix in Cursor Fix in Web


*inappChatHistory = append(*inappChatHistory, chatv2.Message{
MessageId: fmt.Sprintf("openai_%s", contentId),
MessageId: contentId,
Copy link

Choose a reason for hiding this comment

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

Inconsistent message ID prefix removal across handlers

Medium Severity

The PR removes the "openai_" prefix from message IDs in several handlers (utils.go, utils_v2.go, stream.go, toolcall.go) but toolcall_v2.go (not modified in this PR) still uses "openai_toolCall[%d]_%s" format. This creates inconsistent message ID formats within the same conversation. The strings.Contains matching in CreateNewBranch may behave unexpectedly when trying to match messages across these different formats during branch truncation operations.

Additional Locations (1)

Fix in Cursor Fix in Web

<div className="pd-app-tab-content-footer chat-prompt-input noselect rnd-cancel relative">
{/* Only show one popup at a time - priority: prompts > actions > model selection */}
{prompts.length > 0 && <PromptSelection prompts={prompts} />}
{prompt.startsWith("/") && <PromptSelection prompts={prompts} />}
Copy link

Choose a reason for hiding this comment

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

Overlapping popups when typing slash with no prompts

Medium Severity

The comment states "Only show one popup at a time - priority: prompts > actions > model selection" but the logic doesn't enforce this. When a user types "/" and prompts.length === 0 but actions.length > 0, both PromptSelection (showing "No prompts found") and ActionSelection will render simultaneously. The condition on line 104 was changed from prompts.length > 0 to prompt.startsWith("/") but line 105's condition wasn't updated to account for this, causing both components to render when the user types "/" with no matching prompts but available actions.

Fix in Cursor Fix in Web

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: QA / Testing

Development

Successfully merging this pull request may close these issues.

2 participants