Skip to content

feat(ai): real Anthropic/OpenAI streaming + agent routing#117

Merged
jbdevprimary merged 3 commits into
mainfrom
ralph/ws-ai-integration
Feb 12, 2026
Merged

feat(ai): real Anthropic/OpenAI streaming + agent routing#117
jbdevprimary merged 3 commits into
mainfrom
ralph/ws-ai-integration

Conversation

@jbdevprimary
Copy link
Copy Markdown
Contributor

@jbdevprimary jbdevprimary commented Feb 12, 2026

Summary

  • US-010: AIClient interface, AIClientFactory, AIProvider type
  • US-011: AnthropicClient with streaming via @anthropic-ai/sdk
  • US-012: OpenAIClient with streaming via openai SDK
  • US-013: Replaced ChatService mock (simulateAgentResponse) with real AI streaming
  • US-014: Agent-specific system prompts for Architect, Implementer, Reviewer, Tester

The core feature — real AI responses from user's own API keys.

Test plan

  • 22 AI client unit tests passing
  • 15 ChatService tests passing (existing, no regression)
  • 7 AgentPrompts routing tests passing
  • Typecheck passes

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 noreply@anthropic.com

Summary by CodeRabbit

Release Notes

  • New Features

    • Added support for multiple AI providers (Anthropic and OpenAI)
    • Implemented real-time streaming responses from AI agents
    • Added agent-specific system prompts for architect, implementer, reviewer, and tester roles
  • Tests

    • Comprehensive test coverage for AI client implementations and agent prompt routing

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Feb 12, 2026

Warning

Rate limit exceeded

@jbdevprimary has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 25 minutes and 10 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, 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 have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

Introduces a new AI service layer with provider-agnostic abstractions, concrete implementations for Anthropic and OpenAI clients, a factory for instantiating clients by provider, agent-specific system prompts, and integrates real streaming AI responses into ChatService to replace simulated logic.

Changes

Cohort / File(s) Summary
AI Type Definitions
src/services/ai/types.ts
Establishes core abstractions: AIProvider enum, AIMessage/AIStreamChunk interfaces, and AIClient contract with sendMessage and streamMessage methods.
AI Client Implementations
src/services/ai/AnthropicClient.ts, src/services/ai/OpenAIClient.ts
Concrete AIClient implementations for Anthropic and OpenAI SDKs. Both support full-message and streaming modes with AbortSignal cancellation and system message filtering.
AI Service Utilities
src/services/ai/AIClientFactory.ts, src/services/ai/AgentPrompts.ts, src/services/ai/index.ts
Factory function for provider-based client instantiation, agent-specific system prompts (architect, implementer, reviewer, tester), and central export module.
AI Service Tests
src/services/ai/__tests__/AIClientFactory.test.ts, src/services/ai/__tests__/AgentPrompts.test.ts, src/services/ai/__tests__/AnthropicClient.test.ts, src/services/ai/__tests__/OpenAIClient.test.ts
Comprehensive unit test suites validating factory routing, prompt retrieval, client initialization, message filtering, streaming behavior, and AbortSignal propagation.
Chat Service Integration
src/services/chat/ChatService.ts
Replaces simulated AI logic with real streaming via credential resolution, AI client instantiation, system prompt injection, and streaming chunk handling. Adds error handling for missing credentials and API failures. Removes placeholder helper functions.

Sequence Diagram

sequenceDiagram
    actor User
    participant ChatService
    participant CredentialService
    participant AIClientFactory
    participant AIClient as AIClient<br/>(Anthropic/OpenAI)
    participant ExternalAPI as External AI API

    User->>ChatService: requestAgentResponse()
    ChatService->>CredentialService: resolveAICredentials()
    CredentialService-->>ChatService: {provider, apiKey}
    ChatService->>AIClientFactory: createAIClient(provider, apiKey)
    AIClientFactory-->>ChatService: AIClient instance
    
    ChatService->>ChatService: Build AI messages + system prompt
    ChatService->>ChatService: Emit message_start event
    
    ChatService->>AIClient: streamMessage(messages, systemPrompt, onChunk, signal)
    AIClient->>ExternalAPI: Stream request with formatted payload
    
    loop For each text delta
        ExternalAPI-->>AIClient: Chunk with text content
        AIClient->>ChatService: onChunk({text, done: false})
        ChatService->>ChatService: Append chunk to message
        ChatService->>ChatService: Emit message_delta event
    end
    
    ExternalAPI-->>AIClient: Stream complete
    AIClient->>ChatService: onChunk({text: '', done: true})
    ChatService->>ChatService: Emit message_complete event
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 Hop, hop, hooray! AI streams flow today,
Anthropic and OpenAI join the play,
No more fake responses, now real thoughts take flight,
ChatService dancing with credentials held tight,
This rabbit approves—credentials in sight!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title directly and accurately summarizes the main changes: implementing real Anthropic and OpenAI streaming integrations plus agent-specific routing.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch ralph/ws-ai-integration

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.

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello @jbdevprimary, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request introduces a fundamental shift in how AI responses are generated within the application, transitioning from simulated interactions to direct, real-time streaming from Anthropic and OpenAI models. It establishes a robust, provider-agnostic AI client architecture and integrates specialized system prompts to empower distinct agent personalities, significantly enhancing the intelligence and responsiveness of the platform's AI-driven features.

Highlights

  • AI Client Abstraction: Introduced a new AIClient interface, AIProvider type, and AIClientFactory for flexible, provider-agnostic AI model integration.
  • Real-time AI Streaming: Implemented dedicated AnthropicClient and OpenAIClient classes, enabling direct communication and streaming responses from their respective APIs.
  • Agent-Specific Prompts: Added AgentPrompts to define unique system prompts for roles like Architect, Implementer, Reviewer, and Tester, tailoring AI behavior to specific tasks.
  • ChatService Integration: Replaced the mock AI response mechanism in ChatService with live AI streaming, including dynamic resolution of user-provided API keys and conversion of chat history to AI-compatible message formats.
  • Comprehensive Testing: Included extensive unit tests for the new AI client factory, agent prompts, and both Anthropic and OpenAI client implementations to ensure reliability.
Changelog
  • src/services/ai/AIClientFactory.ts
    • Added a factory to create AI client instances based on the specified provider.
  • src/services/ai/AgentPrompts.ts
    • Added a module defining specialized system prompts for various agent roles.
  • src/services/ai/AnthropicClient.ts
    • Added an Anthropic AI client implementation supporting message sending and streaming.
  • src/services/ai/OpenAIClient.ts
    • Added an OpenAI AI client implementation supporting message sending and streaming.
  • src/services/ai/tests/AIClientFactory.test.ts
    • Added unit tests for the AI client factory.
  • src/services/ai/tests/AgentPrompts.test.ts
    • Added unit tests for agent system prompts.
  • src/services/ai/tests/AnthropicClient.test.ts
    • Added unit tests for the Anthropic AI client.
  • src/services/ai/tests/OpenAIClient.test.ts
    • Added unit tests for the OpenAI AI client.
  • src/services/ai/index.ts
    • Added an index file to export AI service components.
  • src/services/ai/types.ts
    • Added type definitions for AI clients, messages, and providers.
  • src/services/chat/ChatService.ts
    • Imported AI client and credential management utilities.
    • Added methods to resolve AI credentials from secure storage and convert chat messages to AI-compatible formats.
    • Updated the requestAgentResponse method to integrate real AI streaming, dynamically resolve API keys, and apply agent-specific system prompts.
    • Removed the simulateAgentResponse and getAgentResponsePlaceholder mock functions.
    • Removed the private delay helper function.
Activity
  • jbdevprimary created the pull request to introduce real Anthropic/OpenAI streaming and agent routing.
  • jbdevprimary outlined the summary of changes, including the new AIClient interface, Anthropic/OpenAI client implementations, replacement of ChatService mock, and agent-specific system prompts.
  • jbdevprimary provided a test plan confirming passing unit tests for AI clients, ChatService, AgentPrompts routing, and successful typechecking.
  • The pull request was generated with Claude Code and co-authored by Claude Opus 4.6.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@amazon-q-developer amazon-q-developer Bot left a comment

Choose a reason for hiding this comment

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

Summary

This PR implements real AI integration with Anthropic and OpenAI, replacing mock responses with actual streaming. The architecture is well-designed with proper abstraction layers, and tests provide good coverage.

Critical Issues Found

Security (2): API keys are exposed in browser via dangerouslyAllowBrowser: true in both AI clients. This is a critical security vulnerability that must be addressed before merge.

Logic Errors (2): Credential resolution logic has fall-through bugs when API keys are configured but empty, leading to wrong provider selection or misleading error messages.

Race Condition (1): Direct Zustand store array mutation causes race conditions during streaming, potentially losing message chunks.

Missing Error Handling (2): Stream failures in both AI clients propagate uncaught, leaving UI in broken state.

All 7 critical issues have actionable fixes provided. Please address these before merging.


You can now have the agent implement changes and create commits directly on your pull request's source branch. Simply comment with /q followed by your request in natural language to ask the agent to make changes.

Comment on lines +19 to +22
this.client = new Anthropic({
apiKey,
dangerouslyAllowBrowser: true,
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛑 Security Vulnerability: API keys are exposed in browser environment via dangerouslyAllowBrowser: true. This bypasses CORS protections and exposes credentials client-side, allowing malicious scripts or browser extensions to steal API keys.1

Implement a secure backend proxy to handle AI API calls instead of calling the Anthropic API directly from the browser.

Footnotes

  1. CWE-522: Insufficiently Protected Credentials - https://cwe.mitre.org/data/definitions/522.html

Comment on lines +18 to +21
this.client = new OpenAI({
apiKey,
dangerouslyAllowBrowser: true,
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛑 Security Vulnerability: API keys are exposed in browser environment via dangerouslyAllowBrowser: true. This bypasses CORS protections and exposes credentials client-side, allowing malicious scripts or browser extensions to steal API keys.1

Implement a secure backend proxy to handle AI API calls instead of calling the OpenAI API directly from the browser.

Footnotes

  1. CWE-522: Insufficiently Protected Credentials - https://cwe.mitre.org/data/definitions/522.html

Comment on lines +292 to +298
// Update message content in the store
const store = useChatStore.getState();
const threadMessages = store.messages[threadId] || [];
const messageIndex = threadMessages.findIndex((m) => m.id === messageId);
if (messageIndex !== -1) {
threadMessages[messageIndex].content = currentContent;
}
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.

🛑 Race Condition: Directly mutating the Zustand store array causes race conditions during concurrent streaming. Multiple chunks can arrive simultaneously, causing state mutations to be lost or overwrite each other, resulting in incomplete message content.

Use Zustand's immutable update pattern with a dedicated action to ensure atomic updates.

Suggested change
// Update message content in the store
const store = useChatStore.getState();
const threadMessages = store.messages[threadId] || [];
const messageIndex = threadMessages.findIndex((m) => m.id === messageId);
if (messageIndex !== -1) {
threadMessages[messageIndex].content = currentContent;
}
// Update message content in the store
useChatStore.getState().updateMessageContent?.(messageId, threadId, currentContent);

Comment on lines +44 to +70
async streamMessage(
messages: AIMessage[],
systemPrompt: string,
onChunk: (chunk: AIStreamChunk) => void,
signal?: AbortSignal
): Promise<void> {
const stream = await this.client.chat.completions.create(
{
model: this.model,
stream: true,
messages: [
{ role: 'system', content: systemPrompt },
...messages.filter((m) => m.role !== 'system'),
],
},
{ signal }
);

for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta?.content;
if (delta) {
onChunk({ text: delta, done: false });
}
}

onChunk({ text: '', done: true });
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Missing error handling for API failures. If the stream encounters an error (network failure, rate limiting, invalid API key), the error propagates uncaught, leaving the UI in a pending state with no error message shown to the user.

Suggested change
async streamMessage(
messages: AIMessage[],
systemPrompt: string,
onChunk: (chunk: AIStreamChunk) => void,
signal?: AbortSignal
): Promise<void> {
const stream = await this.client.chat.completions.create(
{
model: this.model,
stream: true,
messages: [
{ role: 'system', content: systemPrompt },
...messages.filter((m) => m.role !== 'system'),
],
},
{ signal }
);
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta?.content;
if (delta) {
onChunk({ text: delta, done: false });
}
}
onChunk({ text: '', done: true });
}
async streamMessage(
messages: AIMessage[],
systemPrompt: string,
onChunk: (chunk: AIStreamChunk) => void,
signal?: AbortSignal
): Promise<void> {
try {
const stream = await this.client.chat.completions.create(
{
model: this.model,
stream: true,
messages: [
{ role: 'system', content: systemPrompt },
...messages.filter((m) => m.role !== 'system'),
],
},
{ signal }
);
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta?.content;
if (delta) {
onChunk({ text: delta, done: false });
}
}
onChunk({ text: '', done: true });
} catch (error) {
if (signal?.aborted) {
throw new DOMException('Aborted', 'AbortError');
}
throw error;
}
}

Comment on lines +209 to +214
if (openaiMeta) {
const result = await CredentialService.retrieve('openai');
if (result.secret) {
return { provider: 'openai', apiKey: result.secret };
}
}
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.

🛑 Logic Error: If OpenAI credential exists but result.secret is empty/falsy, the function throws a generic "No AI API key configured" error instead of indicating that the OpenAI key is configured but invalid. This misleads users into thinking no key exists when one is actually configured.

Suggested change
if (openaiMeta) {
const result = await CredentialService.retrieve('openai');
if (result.secret) {
return { provider: 'openai', apiKey: result.secret };
}
}
if (openaiMeta) {
const result = await CredentialService.retrieve('openai');
if (result.secret) {
return { provider: 'openai', apiKey: result.secret };
}
throw new Error('OpenAI API key is configured but empty. Please update it in Settings.');
}

Comment on lines +56 to +79
const stream = this.client.messages.stream(
{
model: this.model,
max_tokens: MAX_TOKENS,
system: systemPrompt,
messages: messages
.filter((m) => m.role !== 'system')
.map((m) => ({
role: m.role as 'user' | 'assistant',
content: m.content,
})),
},
{ signal }
);

stream.on('text', (textDelta) => {
onChunk({ text: textDelta, done: false });
});

// Wait for the stream to complete
await stream.finalMessage();

onChunk({ text: '', done: true });
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Missing error handling for API failures. If the stream encounters an error (network failure, rate limiting, invalid API key), the error propagates uncaught, leaving the UI in a pending state with no error message shown to the user.

Suggested change
const stream = this.client.messages.stream(
{
model: this.model,
max_tokens: MAX_TOKENS,
system: systemPrompt,
messages: messages
.filter((m) => m.role !== 'system')
.map((m) => ({
role: m.role as 'user' | 'assistant',
content: m.content,
})),
},
{ signal }
);
stream.on('text', (textDelta) => {
onChunk({ text: textDelta, done: false });
});
// Wait for the stream to complete
await stream.finalMessage();
onChunk({ text: '', done: true });
}
async streamMessage(
messages: AIMessage[],
systemPrompt: string,
onChunk: (chunk: AIStreamChunk) => void,
signal?: AbortSignal
): Promise<void> {
try {
const stream = this.client.messages.stream(
{
model: this.model,
max_tokens: MAX_TOKENS,
system: systemPrompt,
messages: messages
.filter((m) => m.role !== 'system')
.map((m) => ({
role: m.role as 'user' | 'assistant',
content: m.content,
})),
},
{ signal }
);
stream.on('text', (textDelta) => {
onChunk({ text: textDelta, done: false });
});
// Wait for the stream to complete
await stream.finalMessage();
onChunk({ text: '', done: true });
} catch (error) {
if (signal?.aborted) {
throw new DOMException('Aborted', 'AbortError');
}
throw error;
}
}

Comment on lines +202 to +207
if (anthropicMeta) {
const result = await CredentialService.retrieve('anthropic');
if (result.secret) {
return { provider: 'anthropic', apiKey: result.secret };
}
}
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.

🛑 Logic Error: If both Anthropic and OpenAI credentials exist but result.secret is empty/falsy (e.g., empty string stored), the function falls through to the next provider check instead of throwing an error. This causes the wrong provider to be selected or a misleading error message.

Suggested change
if (anthropicMeta) {
const result = await CredentialService.retrieve('anthropic');
if (result.secret) {
return { provider: 'anthropic', apiKey: result.secret };
}
}
// Try anthropic first, then openai
if (anthropicMeta) {
const result = await CredentialService.retrieve('anthropic');
if (result.secret) {
return { provider: 'anthropic', apiKey: result.secret };
}
throw new Error('Anthropic API key is configured but empty. Please update it in Settings.');
}


// We need to get the stream mock to check its call
const Anthropic = jest.requireMock('@anthropic-ai/sdk').default;
const instance = new Anthropic();
@github-actions
Copy link
Copy Markdown

github-actions Bot commented Feb 12, 2026

🚀 Expo preview is ready!

  • Project → thumbcode
  • Platforms → android, ios
  • Scheme → thumbcode
  • Runtime Version → 0.1.0
  • More info

Learn more about 𝝠 Expo Github Action

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

Code Review

This pull request introduces real-time streaming AI responses from Anthropic and OpenAI, replacing the previous mock implementation. It includes a new AI client factory, provider-specific client implementations, and agent-specific system prompts. The integration in ChatService correctly handles credential resolution, message formatting, and streaming updates, following best practices for client-side AI integration using SecureStore for keys. However, there is a security concern regarding error handling where sensitive API keys could be leaked into insecure storage (AsyncStorage) if they are included in error messages from the AI providers.

Comment on lines +323 to +333
const errorContent =
error.message.includes('API key') || error.message.includes('No AI')
? error.message
: `Failed to get response: ${error.message}`;

useChatStore.getState().addMessage({
threadId,
sender: 'system',
content: errorContent,
contentType: 'text',
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

security-medium medium

The error handling logic here potentially leaks sensitive API keys into insecure storage. If an error occurs that includes the API key in its message (e.g., from the AI SDK), the errorContent will contain the full message and be added to the chatStore. Since chatStore persists messages to AsyncStorage (which is unencrypted), this results in credentials being moved from SecureStore to insecure storage.

Consider sanitizing the error message or using a generic message for authentication-related errors.

Suggested change
const errorContent =
error.message.includes('API key') || error.message.includes('No AI')
? error.message
: `Failed to get response: ${error.message}`;
useChatStore.getState().addMessage({
threadId,
sender: 'system',
content: errorContent,
contentType: 'text',
});
const errorContent =
error.message.includes('API key') || error.message.includes('No AI')
? 'Authentication error: Please check your AI API key in Settings.'
: `Failed to get response: ${error.message}`;
useChatStore.getState().addMessage({
threadId,
sender: 'system',
content: errorContent,
contentType: 'text',
});

constructor(apiKey: string, model: string = DEFAULT_MODEL) {
this.client = new Anthropic({
apiKey,
dangerouslyAllowBrowser: 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.

medium

The dangerouslyAllowBrowser: true option is used for the Anthropic SDK. While this might be necessary for certain client-side environments (like React Native's WebView), it's generally discouraged for security reasons as it can expose API keys if not handled with extreme care. Please ensure that API keys are always securely managed and never directly exposed in client-side code bundles. If this is intended for a secure client-side environment, a comment justifying its use would be beneficial.

constructor(apiKey: string, model: string = DEFAULT_MODEL) {
this.client = new OpenAI({
apiKey,
dangerouslyAllowBrowser: 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.

medium

Similar to the Anthropic client, dangerouslyAllowBrowser: true is used here. Please ensure that API keys are securely managed and not exposed in client-side code. If this is a deliberate choice for a secure client-side environment, consider adding a comment to explain the rationale.

// Get thread context (recent messages for context)
const messages = this.getMessages(threadId);
const recentMessages = messages.slice(-10); // Last 10 messages for context
const recentMessages = messages.slice(-10);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

The number 10 for recentMessages is a magic number. It would be more maintainable and readable to define this as a named constant, perhaps at the top of the file or in a configuration object, to clearly indicate its purpose and allow for easier modification.

Suggested change
const recentMessages = messages.slice(-10);
const recentMessages = messages.slice(-MAX_CONTEXT_MESSAGES);

jbdevprimary and others added 3 commits February 11, 2026 22:34
…ntations

Create provider-agnostic AIClient interface with types for messages and
streaming chunks. Implement AnthropicClient using @anthropic-ai/sdk with
message streaming via the stream() API, and OpenAIClient using the openai
SDK with async iterator streaming. Add AIClientFactory for provider-based
client creation. Includes comprehensive unit tests for all components.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Remove simulateAgentResponse() and getAgentResponsePlaceholder() mock
methods. Integrate AIClientFactory with CredentialService to resolve the
user's AI provider and API key from secure storage. Stream real AI
responses through the existing message delta event system. Add error
handling that displays helpful messages when no API key is configured.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Create AgentPrompts module with specialized system prompts for each agent
type: Architect (system design), Implementer (code generation), Reviewer
(code review), and Tester (test writing). Wire ChatService to use
agent-specific prompts when calling the AI client, giving each agent a
distinct personality and area of expertise.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@jbdevprimary jbdevprimary force-pushed the ralph/ws-ai-integration branch from 19d6483 to ad02d68 Compare February 12, 2026 04:34
@jbdevprimary jbdevprimary merged commit c6c31bd into main Feb 12, 2026
7 checks passed
@jbdevprimary jbdevprimary deleted the ralph/ws-ai-integration branch February 12, 2026 04:34
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 Fix all issues with AI agents
In `@src/services/ai/__tests__/AnthropicClient.test.ts`:
- Around line 141-160: The test currently doesn't assert that the abort signal
is forwarded to the SDK stream; after calling AnthropicClient.streamMessage,
inspect the mocked stream call args and assert the signal was passed.
Concretely, use the mocked SDK (jest.requireMock('@anthropic-ai/sdk').default)
to access the created mock instance (e.g., Anthropic.mock.instances[0]) or
assert against mockStream.mock.calls to find the call from
testClient.streamMessage and expect the last argument to equal
controller.signal; keep existing mocks (mockStreamOn, mockStreamFinalMessage)
and the onChunk assertion but add the signal assertion to verify proper
propagation.
- Around line 111-139: The test currently races handlers vs final message so
text chunks are never captured; update the mocked stream behavior in the test
for client.streamMessage by either calling the provided handler synchronously
inside mockStreamOn (invoke handler('Hello') and handler(' world') immediately
when event === 'text') or by changing mockStreamFinalMessage to resolve only
after the queued text callbacks have executed (e.g., call handlers then resolve
mockStreamFinalMessage), so that onChunk receives the 'Hello' and ' world'
chunks before the final { text: '', done: true } is appended; adjust assertions
on the chunks array accordingly to verify all text chunks plus the done chunk.

In `@src/services/ai/AnthropicClient.ts`:
- Around line 70-79: The final "done" chunk (onChunk({ text: '', done: true }))
is never emitted if stream.finalMessage() rejects (due to abort or error); wrap
the await stream.finalMessage() and subsequent onChunk call in a try/finally so
onChunk({ text: '', done: true }) is always called regardless of errors/abort.
Update the block around stream.on('text', ...) and the await
stream.finalMessage() in AnthropicClient.ts to use try/finally (or equivalent)
so the final done signal is emitted even when stream.finalMessage() throws or
the abort signal fires.
- Around line 19-22: Rename the file containing the AnthropicClient class from
"AnthropicClient.ts" to "anthropic-client.ts" to follow kebab-case; then update
the export in src/services/ai/index.ts to import/export the class from
"anthropic-client" (and update any other imports that reference
"AnthropicClient" accordingly), ensuring the class name AnthropicClient and the
usage of new Anthropic({ apiKey, dangerouslyAllowBrowser: true }) remain
unchanged.

In `@src/services/ai/OpenAIClient.ts`:
- Around line 62-69: The final "done" chunk is skipped if the streaming loop in
OpenAIClient throws or is aborted; wrap the "for await (const chunk of stream)"
loop in a try/finally and call onChunk({ text: '', done: true }) in the finally
block so the done signal is always emitted (refer to the stream variable and
onChunk callback in the OpenAIClient streaming method).

In `@src/services/chat/ChatService.ts`:
- Around line 290-298: Currently the code directly mutates the store array by
assigning to threadMessages[messageIndex].content which bypasses Zustand's
set/subscription mechanism; replace this with a call to a store action (e.g.,
add an updateMessageContent action on useChatStore) that performs an immutable
update via set(...) to replace the message in messages[threadId] (mapping over
state.messages[threadId] and returning { ...m, content } for the matching
messageId), and invoke useChatStore.getState().updateMessageContent(messageId,
threadId, currentContent) instead of mutating threadMessages in-place.
🧹 Nitpick comments (4)
src/services/ai/AgentPrompts.ts (1)

10-10: Use a stricter key type for AGENT_PROMPTS.

Record<string, string> loses compile-time safety. If MessageSender values change, this map won't flag missing or extraneous keys. Consider a Partial<Record<MessageSender, string>> (or a full Record if every sender should have a prompt).

Proposed fix
-const AGENT_PROMPTS: Record<string, string> = {
+const AGENT_PROMPTS: Partial<Record<MessageSender, string>> = {
src/services/chat/ChatService.ts (2)

258-261: Hardcoded context window of 10 messages.

The slice(-10) limit should be a named constant for discoverability and tuning. Also, 10 messages may be insufficient for complex multi-turn agent interactions, and there's no token-budget awareness — a few long code messages could blow past the model's context window even within 10 messages.

Suggested improvement
+const MAX_CONTEXT_MESSAGES = 10;
+
 // ...
-      const recentMessages = messages.slice(-10);
+      const recentMessages = messages.slice(-MAX_CONTEXT_MESSAGES);

320-340: Error handling looks solid but relies on fragile string matching.

The error.message.includes('API key') || error.message.includes('No AI') check on line 324 couples error presentation to exact wording in resolveAICredentials. If that error message changes, users will get the generic fallback instead of the helpful message.

Consider introducing a typed error class (e.g., AICredentialError) and using instanceof instead:

Suggested approach
class AICredentialError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'AICredentialError';
  }
}

Then in the catch block:

-        const errorContent =
-          error.message.includes('API key') || error.message.includes('No AI')
-            ? error.message
-            : `Failed to get response: ${error.message}`;
+        const errorContent =
+          error instanceof AICredentialError
+            ? error.message
+            : `Failed to get response: ${error.message}`;
src/services/ai/__tests__/AnthropicClient.test.ts (1)

54-54: Consider updating to the latest Claude Sonnet model.

The test correctly asserts 'claude-sonnet-4-20250514' matching the DEFAULT_MODEL constant in AnthropicClient.ts. The model identifier is valid, but it is not the current Sonnet line—Claude Sonnet 4.5 (claude-sonnet-4-5-20250929) is now the latest generation. The current model has a tentative retirement not sooner than May 14, 2026. If you need the newest features and performance, consider updating to claude-sonnet-4-5-20250929 or the moving alias claude-sonnet-4-5.

Comment on lines +111 to +139
it('should stream message chunks and call onChunk', async () => {
const chunks: AIStreamChunk[] = [];
const onChunk = (chunk: AIStreamChunk) => chunks.push(chunk);

// Simulate the stream: when 'text' event handler is registered, call it
mockStreamOn.mockImplementation(function (
this: { on: typeof mockStreamOn; finalMessage: typeof mockStreamFinalMessage },
event: string,
handler: (text: string) => void
) {
if (event === 'text') {
// Queue the text callbacks
setTimeout(() => {
handler('Hello');
handler(' world');
}, 0);
}
return this;
});

mockStreamFinalMessage.mockResolvedValue({
content: [{ type: 'text', text: 'Hello world' }],
});

await client.streamMessage(testMessages, testSystemPrompt, onChunk);

// The done chunk is always sent
expect(chunks[chunks.length - 1]).toEqual({ text: '', done: true });
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Stream test doesn't actually verify text chunk delivery.

Because mockStreamFinalMessage resolves as a microtask while the setTimeout(() => { handler('Hello'); handler(' world'); }, 0) fires as a macrotask, the text handlers execute after streamMessage has already returned. This means chunks will only contain the final { text: '', done: true } — the 'Hello' and ' world' chunks are never captured.

Consider invoking the handler synchronously within mockStreamOn or using a deferred pattern where finalMessage resolves only after the text callbacks have fired:

Proposed fix
      mockStreamOn.mockImplementation(function (
        this: { on: typeof mockStreamOn; finalMessage: typeof mockStreamFinalMessage },
        event: string,
        handler: (text: string) => void
      ) {
        if (event === 'text') {
-          // Queue the text callbacks
-          setTimeout(() => {
-            handler('Hello');
-            handler(' world');
-          }, 0);
+          // Call handlers synchronously so they fire before finalMessage resolves
+          handler('Hello');
+          handler(' world');
        }
        return this;
      });

Then assert all chunks were received:

-      // The done chunk is always sent
-      expect(chunks[chunks.length - 1]).toEqual({ text: '', done: true });
+      expect(chunks).toEqual([
+        { text: 'Hello', done: false },
+        { text: ' world', done: false },
+        { text: '', done: true },
+      ]);
🤖 Prompt for AI Agents
In `@src/services/ai/__tests__/AnthropicClient.test.ts` around lines 111 - 139,
The test currently races handlers vs final message so text chunks are never
captured; update the mocked stream behavior in the test for client.streamMessage
by either calling the provided handler synchronously inside mockStreamOn (invoke
handler('Hello') and handler(' world') immediately when event === 'text') or by
changing mockStreamFinalMessage to resolve only after the queued text callbacks
have executed (e.g., call handlers then resolve mockStreamFinalMessage), so that
onChunk receives the 'Hello' and ' world' chunks before the final { text: '',
done: true } is appended; adjust assertions on the chunks array accordingly to
verify all text chunks plus the done chunk.

Comment on lines +141 to +160
it('should pass the abort signal to the stream', async () => {
const onChunk = jest.fn();
const controller = new AbortController();

mockStreamOn.mockReturnThis();
mockStreamFinalMessage.mockResolvedValue({
content: [{ type: 'text', text: '' }],
});

// We need to get the stream mock to check its call
const Anthropic = jest.requireMock('@anthropic-ai/sdk').default;
const instance = new Anthropic();

// Create a new client that uses this instance
const testClient = new AnthropicClient('test-key');
await testClient.streamMessage(testMessages, testSystemPrompt, onChunk, controller.signal);

// Verify stream was called (indirectly through the mock)
expect(onChunk).toHaveBeenCalledWith({ text: '', done: true });
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Abort signal test doesn't verify signal propagation.

The test is titled "should pass the abort signal to the stream" but never asserts that stream(...) was called with the signal. Lines 151-153 create a separate Anthropic mock instance that is unrelated to the one used internally by testClient. The only assertion (line 159) checks that the done chunk was emitted, which has nothing to do with signal propagation.

To actually verify signal forwarding, assert against the stream mock's call args:

Proposed approach
      const testClient = new AnthropicClient('test-key');
      await testClient.streamMessage(testMessages, testSystemPrompt, onChunk, controller.signal);

-      // Verify stream was called (indirectly through the mock)
-      expect(onChunk).toHaveBeenCalledWith({ text: '', done: true });
+      // Verify the abort signal was forwarded to the stream call
+      const streamMock = instance.messages.stream;
+      expect(streamMock).toHaveBeenCalledWith(
+        expect.any(Object),
+        expect.objectContaining({ signal: controller.signal })
+      );
🤖 Prompt for AI Agents
In `@src/services/ai/__tests__/AnthropicClient.test.ts` around lines 141 - 160,
The test currently doesn't assert that the abort signal is forwarded to the SDK
stream; after calling AnthropicClient.streamMessage, inspect the mocked stream
call args and assert the signal was passed. Concretely, use the mocked SDK
(jest.requireMock('@anthropic-ai/sdk').default) to access the created mock
instance (e.g., Anthropic.mock.instances[0]) or assert against
mockStream.mock.calls to find the call from testClient.streamMessage and expect
the last argument to equal controller.signal; keep existing mocks (mockStreamOn,
mockStreamFinalMessage) and the onChunk assertion but add the signal assertion
to verify proper propagation.

Comment on lines +19 to +22
this.client = new Anthropic({
apiKey,
dangerouslyAllowBrowser: true,
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# First, explore the services/ai directory to understand the structure
find src/services/ai -type f -name "*.ts" -o -name "*.tsx" | head -20

Repository: agentic-dev-library/thumbcode

Length of output: 450


🏁 Script executed:

# Look for AnthropicClient.ts and related AI clients
fd "AnthropicClient|OpenAIClient" --type f

Repository: agentic-dev-library/thumbcode

Length of output: 236


🏁 Script executed:

# Check how API keys are passed to AI clients - search for instantiation patterns
rg "new Anthropic|new OpenAI|AnthropicClient|OpenAIClient" -A 3 -B 3 --type ts --type tsx

Repository: agentic-dev-library/thumbcode

Length of output: 100


🏁 Script executed:

# Search for expo-secure-store usage in the codebase
rg "expo-secure-store|getSecureData|setSecureData" --type ts --type tsx

Repository: agentic-dev-library/thumbcode

Length of output: 100


🏁 Script executed:

# Check filename conventions in src/services, src/components, src/hooks
ls -la src/services/ 2>/dev/null | head -20
ls -la src/components/ 2>/dev/null | head -20

Repository: agentic-dev-library/thumbcode

Length of output: 1183


🏁 Script executed:

# Read AnthropicClient.ts to see full context
cat -n src/services/ai/AnthropicClient.ts

Repository: agentic-dev-library/thumbcode

Length of output: 2658


🏁 Script executed:

# Read AIClientFactory.ts to understand how API keys are obtained
cat -n src/services/ai/AIClientFactory.ts

Repository: agentic-dev-library/thumbcode

Length of output: 1140


🏁 Script executed:

# Search for expo-secure-store usage without type filter
rg "expo-secure-store|getSecureData|setSecureData|SecureStore" -i

Repository: agentic-dev-library/thumbcode

Length of output: 9797


🏁 Script executed:

# Check actual component filenames for naming pattern
find src/components -maxdepth 2 -type f -name "*.ts" -o -name "*.tsx" | head -20

Repository: agentic-dev-library/thumbcode

Length of output: 753


🏁 Script executed:

# Check if there's a hooks directory and its naming
find src/hooks -type f -name "*.ts" -o -name "*.tsx" 2>/dev/null | head -20

Repository: agentic-dev-library/thumbcode

Length of output: 122


🏁 Script executed:

# Search for where createAIClient is called and how the apiKey is obtained
rg "createAIClient" -B 5 -A 2

Repository: agentic-dev-library/thumbcode

Length of output: 13604


🏁 Script executed:

# Look for CredentialService usage patterns to see if API keys are retrieved before passing to clients
rg "CredentialService|getCredential|API.*key|apiKey" src/ -i | grep -i "anthropic\|openai\|credential" | head -30

Repository: agentic-dev-library/thumbcode

Length of output: 2043


🏁 Script executed:

# Check the specific onboarding flow to see credential retrieval pattern
cat -n app/\(onboarding\)/api-keys.tsx 2>/dev/null || find . -name "api-keys.tsx" -exec cat -n {} \;

Repository: agentic-dev-library/thumbcode

Length of output: 11762


🏁 Script executed:

# Check component file naming patterns
find src/components -maxdepth 3 -type f \( -name "*.tsx" -o -name "*.ts" \) | head -30

Repository: agentic-dev-library/thumbcode

Length of output: 1133


🏁 Script executed:

# Check services file naming patterns
find src/services -maxdepth 3 -type f \( -name "*.ts" -o -name "*.tsx" \) | head -30

Repository: agentic-dev-library/thumbcode

Length of output: 580


🏁 Script executed:

# Look at ChatService.ts to confirm the full API key flow
cat -n src/services/chat/ChatService.ts | head -80

Repository: agentic-dev-library/thumbcode

Length of output: 2584


Rename file to anthropic-client.ts to follow kebab-case naming convention.

Per coding guidelines, file names should use kebab-case (e.g., agent-card.tsx). Rename AnthropicClient.tsanthropic-client.ts. Update the corresponding export in src/services/ai/index.ts.

Note: The dangerouslyAllowBrowser: true flag is necessary and intentional for React Native/Expo context—it's not a security concern. API keys are properly secured via expo-secure-store as required; they flow from CredentialService.retrieve() at runtime and are never stored in code.

🤖 Prompt for AI Agents
In `@src/services/ai/AnthropicClient.ts` around lines 19 - 22, Rename the file
containing the AnthropicClient class from "AnthropicClient.ts" to
"anthropic-client.ts" to follow kebab-case; then update the export in
src/services/ai/index.ts to import/export the class from "anthropic-client" (and
update any other imports that reference "AnthropicClient" accordingly), ensuring
the class name AnthropicClient and the usage of new Anthropic({ apiKey,
dangerouslyAllowBrowser: true }) remain unchanged.

Comment on lines +70 to +79

stream.on('text', (textDelta) => {
onChunk({ text: textDelta, done: false });
});

// Wait for the stream to complete
await stream.finalMessage();

onChunk({ text: '', done: true });
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

onChunk({ done: true }) is skipped if the stream errors or is aborted.

If the abort signal fires or the stream encounters an error, stream.finalMessage() will reject, and the final done chunk on Line 78 is never emitted. Callers relying on the done signal to finalize UI state (e.g., stop a loading indicator) will be left hanging.

Consider wrapping in try/finally:

Proposed fix
     stream.on('text', (textDelta) => {
       onChunk({ text: textDelta, done: false });
     });
 
-    // Wait for the stream to complete
-    await stream.finalMessage();
-
-    onChunk({ text: '', done: true });
+    try {
+      await stream.finalMessage();
+    } finally {
+      onChunk({ text: '', done: true });
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
stream.on('text', (textDelta) => {
onChunk({ text: textDelta, done: false });
});
// Wait for the stream to complete
await stream.finalMessage();
onChunk({ text: '', done: true });
}
stream.on('text', (textDelta) => {
onChunk({ text: textDelta, done: false });
});
try {
await stream.finalMessage();
} finally {
onChunk({ text: '', done: true });
}
}
🤖 Prompt for AI Agents
In `@src/services/ai/AnthropicClient.ts` around lines 70 - 79, The final "done"
chunk (onChunk({ text: '', done: true })) is never emitted if
stream.finalMessage() rejects (due to abort or error); wrap the await
stream.finalMessage() and subsequent onChunk call in a try/finally so onChunk({
text: '', done: true }) is always called regardless of errors/abort. Update the
block around stream.on('text', ...) and the await stream.finalMessage() in
AnthropicClient.ts to use try/finally (or equivalent) so the final done signal
is emitted even when stream.finalMessage() throws or the abort signal fires.

Comment on lines +62 to +69
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta?.content;
if (delta) {
onChunk({ text: delta, done: false });
}
}

onChunk({ text: '', done: true });
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Same done chunk issue as AnthropicClient — final onChunk({ done: true }) is skipped on error/abort.

If the for await loop throws (e.g., abort signal, network error), the done chunk on Line 69 is never emitted. Wrap in try/finally for consistent behavior.

Proposed fix
-    for await (const chunk of stream) {
-      const delta = chunk.choices[0]?.delta?.content;
-      if (delta) {
-        onChunk({ text: delta, done: false });
+    try {
+      for await (const chunk of stream) {
+        const delta = chunk.choices[0]?.delta?.content;
+        if (delta) {
+          onChunk({ text: delta, done: false });
+        }
       }
+    } finally {
+      onChunk({ text: '', done: true });
     }
-
-    onChunk({ text: '', done: true });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta?.content;
if (delta) {
onChunk({ text: delta, done: false });
}
}
onChunk({ text: '', done: true });
try {
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta?.content;
if (delta) {
onChunk({ text: delta, done: false });
}
}
} finally {
onChunk({ text: '', done: true });
}
🤖 Prompt for AI Agents
In `@src/services/ai/OpenAIClient.ts` around lines 62 - 69, The final "done" chunk
is skipped if the streaming loop in OpenAIClient throws or is aborted; wrap the
"for await (const chunk of stream)" loop in a try/finally and call onChunk({
text: '', done: true }) in the finally block so the done signal is always
emitted (refer to the stream variable and onChunk callback in the OpenAIClient
streaming method).

Comment on lines +290 to +298
currentContent += chunk.text;

// Update message content in the store
const store = useChatStore.getState();
const threadMessages = store.messages[threadId] || [];
const messageIndex = threadMessages.findIndex((m) => m.id === messageId);
if (messageIndex !== -1) {
threadMessages[messageIndex].content = currentContent;
}
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 | 🔴 Critical

Direct mutation of Zustand store state — UI won't re-render during streaming.

useChatStore.getState().messages[threadId] returns a reference to the store's internal array. Mutating threadMessages[messageIndex].content in-place bypasses Zustand's subscription mechanism, so components using useChatStore selectors on messages will not re-render as chunks arrive.

You should use a proper store action (e.g., updateMessageContent) that calls set(...) internally:

Proposed fix
-          // Update message content in the store
-          const store = useChatStore.getState();
-          const threadMessages = store.messages[threadId] || [];
-          const messageIndex = threadMessages.findIndex((m) => m.id === messageId);
-          if (messageIndex !== -1) {
-            threadMessages[messageIndex].content = currentContent;
-          }
+          // Update message content via store action to trigger re-renders
+          useChatStore.getState().updateMessageContent(messageId, threadId, currentContent);

If updateMessageContent doesn't exist yet, add it to the chat store with an immutable update:

updateMessageContent: (messageId: string, threadId: string, content: string) =>
  set((state) => ({
    messages: {
      ...state.messages,
      [threadId]: state.messages[threadId].map((m) =>
        m.id === messageId ? { ...m, content } : m
      ),
    },
  })),

Based on learnings: "Use Zustand for state management" — Zustand requires immutable state updates via set() for subscriptions to trigger.

🤖 Prompt for AI Agents
In `@src/services/chat/ChatService.ts` around lines 290 - 298, Currently the code
directly mutates the store array by assigning to
threadMessages[messageIndex].content which bypasses Zustand's set/subscription
mechanism; replace this with a call to a store action (e.g., add an
updateMessageContent action on useChatStore) that performs an immutable update
via set(...) to replace the message in messages[threadId] (mapping over
state.messages[threadId] and returning { ...m, content } for the matching
messageId), and invoke useChatStore.getState().updateMessageContent(messageId,
threadId, currentContent) instead of mutating threadMessages in-place.

@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
45.8% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant