Skip to content

Fix/tool race conditions#258

Open
jherr wants to merge 7 commits intomainfrom
fix/tool-race-conditions
Open

Fix/tool race conditions#258
jherr wants to merge 7 commits intomainfrom
fix/tool-race-conditions

Conversation

@jherr
Copy link
Contributor

@jherr jherr commented Jan 30, 2026

🎯 Changes

Fixes some issues around client side tool execution of multiple tool calls. Adds tests to validate the fixes.

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm run test:pr.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • Bug Fixes

    • Fixed race conditions in concurrent tool execution with improved execution tracking and deduplication safeguards.
    • Enhanced handling of server-side tool completion verification.
  • New Features

    • Added deterministic LLM simulator adapter for testing tool-calling workflows with structured streaming output.
    • Introduced comprehensive tool testing infrastructure with support for approval flows, sequential/parallel tool execution, and error scenarios.
    • Added tool simulator UI in testing panel with real-time chat interactions and debug capabilities.
  • Tests

    • Added extensive test suites covering approval workflows, client/server tools, multi-tool scenarios, and race conditions.
    • Introduced end-to-end Playwright tests for tool interaction workflows.

jherr added 2 commits January 29, 2026 18:38
- Add LLM simulator adapter for deterministic testing of tool flows
- Add comprehensive unit tests for tool execution scenarios:
  - Server tool execution
  - Client tool execution
  - Approval workflows
  - Multi-tool and parallel tool execution
  - Tool sequences
  - Error handling
- Add E2E test infrastructure for tool scenarios
- Fix client tool race conditions by tracking pending tool executions
- Prevent duplicate continuation attempts with continuationPending flag
- Guard against concurrent stream processing in streamResponse
- Add approval info to ToolCall type for server-side decision tracking
- Include approval info in model message conversion for approval workflows
- Check ModelMessage format for approval info extraction in chat activity

This change improves the reliability of tool execution, especially for:
- Client tools with async execute functions
- Approval-based tool workflows
- Sequential tool execution scenarios
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 30, 2026

📝 Walkthrough

Walkthrough

This pull request addresses tool execution race conditions in the AI framework by introducing pending execution tracking, stream concurrency guards, and server-side state preservation. Changes include modifications to chat client streaming and tool handling, server-side message conversion and approval tracking, comprehensive test infrastructure with an LLM simulator adapter, and E2E test suites validating various tool execution scenarios.

Changes

Cohort / File(s) Summary
Core Race Condition Fixes
packages/typescript/ai-client/src/chat-client.ts, packages/typescript/ai/src/activities/chat/index.ts, packages/typescript/ai/src/activities/chat/messages.ts
Introduced pendingToolExecutions tracking and continuationPending flag to prevent concurrent stream processing and duplicate continuation attempts. Changed onToolCall from async to sync with internal promise tracking. Extended message conversion to preserve approvalInfo and client tool results from original UIMessages. Modified tool completion detection to recognize pendingExecution markers and server-side tool-result parts.
Stream Processing & Adapters
packages/typescript/ai-client/src/connection-adapters.ts, packages/typescript/ai/src/activities/chat/stream/processor.ts
Removed client-side UIMessage-to-ModelMessage conversion; messages now pass through unchanged to server. Extended areAllToolsComplete to recognize server-side tool completion via tool-result parts in addition to approval and output checks.
LLM Simulator Infrastructure
packages/typescript/smoke-tests/adapters/src/llm-simulator.ts, packages/typescript/smoke-tests/adapters/package.json, packages/typescript/smoke-tests/adapters/vitest.config.ts
Added comprehensive deterministic LLM simulator adapter with streaming output, tool-call signaling, iteration tracking, and preset scripts (singleServerTool, singleClientTool, approvalTool, sequentialTools, parallelTools, textOnly). Configured Vitest with module aliases and coverage settings.
Tool-Specific Test Suites
packages/typescript/smoke-tests/adapters/src/tests/tools/*.test.ts (approval, client-tool, error-handling, multi-tool, sequences, server-tool)
Introduced six comprehensive test suites validating tool execution scenarios including approval workflows, client-side tool handling, concurrent execution, error cases, sequential and parallel tool combinations, and tool result propagation.
E2E Test Infrastructure
packages/typescript/smoke-tests/e2e/src/routes/api.tools-test.ts, packages/typescript/smoke-tests/e2e/src/routes/tools-test.tsx, packages/typescript/smoke-tests/e2e/tests/tools/*.spec.ts
Added server-side tools test API with 20+ predefined scenarios, React UI with scenario selector and approval/tool execution tracking, and four Playwright E2E test suites validating approval flows, client tools, race conditions, and server-client sequences.
Testing Panel & Routes
testing/panel/src/routes/simulator.tsx, testing/panel/src/routes/api.simulator-chat.ts, testing/panel/src/components/Header.tsx, testing/panel/src/lib/simulator-tools.ts, testing/panel/src/routeTree.gen.ts
Added dedicated simulator page with chat interface, debug panel with chunk inspection, tool injection templates, and corresponding API endpoint with tool-call parsing and SSE streaming. Extended header navigation and created tool definition library with server/client/hybrid variants supporting approval workflows.
Test Configuration & Data
packages/typescript/smoke-tests/adapters/src/harness.ts, packages/typescript/smoke-tests/e2e/package.json, packages/typescript/smoke-tests/e2e/playwright.config.ts, testing/panel/playwright-report/..., testing/panel/test-results/..., testing/panel/tests/tool-flow.spec.ts
Removed tool-call aggregation in approval message building. Updated dependencies (added test-adapters, zod, moved vite-tsconfig-paths). Changed webServer.reuseExistingServer to constant true. Added retry logic for flaky tool-flow tests and updated Playwright report snapshots.
Release Notes
.changeset/tool-race-conditions.md
Documented patch release fixing tool execution race conditions, continuation deduplication, concurrent stream guarding, and approvalInfo propagation for approval-based workflows.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • jherr

Poem

🐰 Hops with joy at race conditions now tamed,
Pending tools tracked, no longer unnamed,
Streams flow safely, continuations aligned,
Approvals preserved, peace of mind we find.
With simulators testing every flow,
Reliability grows, watch the tools go!

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Fix/tool race conditions' clearly and concisely identifies the main objective of the changeset: addressing race condition issues in tool execution.
Description check ✅ Passed The PR description follows the template structure, includes completed checklist items, and adequately describes the changes (fixes for client-side tool execution with multiple concurrent tool calls) and testing approach.
Docstring Coverage ✅ Passed Docstring coverage is 81.40% 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 docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/tool-race-conditions

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.

@jherr jherr marked this pull request as ready for review January 30, 2026 23:52
@jherr jherr requested a review from a team January 30, 2026 23:52
@nx-cloud
Copy link

nx-cloud bot commented Jan 30, 2026

View your CI Pipeline Execution ↗ for commit 91c9c6f

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 2m 59s View ↗
nx run-many --targets=build --exclude=examples/** ✅ Succeeded 1m 14s View ↗

☁️ Nx Cloud last updated this comment at 2026-02-05 05:24:15 UTC

@pkg-pr-new
Copy link

pkg-pr-new bot commented Jan 30, 2026

Open in StackBlitz

@tanstack/ai

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai@258

@tanstack/ai-anthropic

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-anthropic@258

@tanstack/ai-client

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-client@258

@tanstack/ai-devtools-core

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-devtools-core@258

@tanstack/ai-gemini

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-gemini@258

@tanstack/ai-grok

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-grok@258

@tanstack/ai-ollama

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-ollama@258

@tanstack/ai-openai

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-openai@258

@tanstack/ai-openrouter

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-openrouter@258

@tanstack/ai-preact

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-preact@258

@tanstack/ai-react

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-react@258

@tanstack/ai-react-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-react-ui@258

@tanstack/ai-solid

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-solid@258

@tanstack/ai-solid-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-solid-ui@258

@tanstack/ai-svelte

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-svelte@258

@tanstack/ai-vue

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vue@258

@tanstack/ai-vue-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vue-ui@258

@tanstack/preact-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/preact-ai-devtools@258

@tanstack/react-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/react-ai-devtools@258

@tanstack/solid-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/solid-ai-devtools@258

commit: 91c9c6f

Copy link
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: 12

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/typescript/ai/src/activities/chat/messages.ts (1)

178-187: ⚠️ Potential issue | 🟠 Major

Preserve approval metadata when converting ModelMessage → UIMessage.

Right now approvals on toolCall.approval are dropped, so approval state/decision is lost in UI conversions.

Suggested fix
-      parts.push({
-        type: 'tool-call',
-        id: toolCall.id,
-        name: toolCall.function.name,
-        arguments: toolCall.function.arguments,
-        state: 'input-complete', // Model messages have complete arguments
-      })
+      const approval = toolCall.approval
+        ? {
+            id: toolCall.approval.id,
+            needsApproval: true,
+            approved: toolCall.approval.approved,
+          }
+        : undefined
+      parts.push({
+        type: 'tool-call',
+        id: toolCall.id,
+        name: toolCall.function.name,
+        arguments: toolCall.function.arguments,
+        state: approval ? 'approval-responded' : 'input-complete',
+        ...(approval && { approval }),
+      })
🤖 Fix all issues with AI agents
In `@packages/typescript/ai-client/src/chat-client.ts`:
- Around line 317-320: The current guard in streamResponse() returns early when
isLoading is true, so calls to append() can add messages without triggering a
response; implement a follow-up queue: when append() is called while isLoading
is true, push a lightweight marker (e.g., a pendingStream flag or enqueue the
message id) onto a small queue/flag on the ChatClient instance and, when the
active stream finishes (in the completion/cleanup path of streamResponse()),
check that queue/flag and immediately call streamResponse() again to process the
queued append; update append(), streamResponse(), and the stream completion
handler (look for methods named append, streamResponse, and the isLoading
checks) to set/enqueue on append and to dequeue/clear and re-invoke
streamResponse after the current load completes.

In `@packages/typescript/smoke-tests/adapters/src/tests/tools/sequences.test.ts`:
- Line 301: The test declares callOrder with the disallowed shorthand type
`string[]`; update each declaration of `callOrder` (e.g., the `const callOrder:
string[] = []` instances around the tests) to use the preferred generic form
`Array<string>` so the lint rule passes (replace `string[]` with `Array<string>`
in both occurrences).

In `@packages/typescript/smoke-tests/e2e/package.json`:
- Around line 25-30: Package.json currently lists build-only plugin
"vite-tsconfig-paths" alongside runtime deps; move only "vite-tsconfig-paths"
into devDependencies. Edit package.json to remove "vite-tsconfig-paths" from the
dependencies block and add it under devDependencies, while keeping
"@tanstack/tests-adapters" and "zod" in dependencies because
"@tanstack/tests-adapters" is imported at runtime in
src/routes/api.tools-test.ts and "zod" is used at runtime in
src/routes/api.tools-test.ts and src/routes/tools-test.tsx; ensure package.json
retains correct versions for react/react-dom/tailwindcss and other runtime deps.

In `@packages/typescript/smoke-tests/e2e/src/routes/tools-test.tsx`:
- Around line 355-370: The click handler for the approve button currently uses
non-null assertions (tc.approval!.id!) which can throw; instead, read const
approval = tc.approval and const approvalId = approval?.id and early-return if
approvalId is falsy, then use approvalId for
respondedApprovals.current.has(...), respondedApprovals.current.add(...),
addEvent({... toolCallId: tc.id ...}) and addToolApprovalResponse({ id:
approvalId, approved: true }); update the handler in the approve button (the
onClick closure) to use these guarded variables and avoid any `!` assertions.

In `@packages/typescript/smoke-tests/e2e/tests/tools/race-conditions.spec.ts`:
- Line 1: Reorder and split the Playwright import so named exports are
alphabetized and the Page type is a type-only import: change the combined import
of "test, expect, type Page" into two imports — a value import with alphabetized
members (expect, test) and a separate type-only import for Page using "import
type { Page }" referencing the existing import statement in the file.

In `@testing/panel/src/routes/api.simulator-chat.ts`:
- Around line 5-12: The import list at the top is failing the sort-imports rule;
reorder the named imports from '@/lib/simulator-tools' into ascending
alphabetical order to satisfy ESLint (e.g., arrange clientServerTool,
clientServerToolWithApproval, clientToolDef, clientToolWithApprovalDef,
serverTool, serverToolWithApproval). Update the single import statement that
contains serverTool, serverToolWithApproval, clientServerTool,
clientServerToolWithApproval, clientToolDef, and clientToolWithApprovalDef so
the specifiers are alphabetized and then run the linter to confirm the error is
resolved.
- Around line 25-32: Replace the T[] bracket-style array type with the generic
Array<T> form: update the declaration of toolCalls and the function return type
in parseToolCalls from ParsedToolCall[] to Array<ParsedToolCall> (references:
ParsedToolCall type, parseToolCalls function, and the toolCalls variable) so the
file complies with the `@typescript-eslint/array-type` rule.
- Around line 23-55: The regex TOOL_CALL_REGEX is declared with the global flag,
so its internal lastIndex can persist between calls and cause missed matches;
inside parseToolCalls(message) reset TOOL_CALL_REGEX.lastIndex = 0 before
entering the while loop so each invocation starts scanning from the beginning of
the message, then proceed with the existing matching/JSON-parsing logic in
parseToolCalls.

In `@testing/panel/src/routes/simulator.tsx`:
- Around line 232-237: The UI directly calls JSON.parse on part.arguments inside
the JSX (the JSON.stringify(JSON.parse(part.arguments), null, 2) expression)
which will throw if the string is invalid; wrap the parse in a safe path: either
create a small helper (e.g., safeParseJSON) or run a try/catch around JSON.parse
to fall back to a sensible value (the raw part.arguments string or an error
message) before passing to JSON.stringify, and update the JSX to render that
safe result so invalid JSON doesn't crash the component.
- Around line 3-11: Reorder the named imports from 'lucide-react' so they
satisfy the project's sort rule: replace the current list (Send, Square, Zap,
FlaskConical, Server, Monitor, ShieldCheck) with an alphabetically-sorted list:
FlaskConical, Monitor, Send, Server, ShieldCheck, Square, Zap; apply the same
reordering to the other identical import on the other occurrence (lines
referenced in the review).
- Around line 202-209: The code registers remarkGfm in rehypePlugins so GFM
won't be applied; move remarkGfm into the remarkPlugins array for the
ReactMarkdown component. Locate the ReactMarkdown usage and remove remarkGfm
from rehypePlugins (which should keep rehypeRaw, rehypeSanitize,
rehypeHighlight) and add remarkGfm to remarkPlugins so that remarkGfm runs
during Markdown AST processing.
- Around line 488-499: The dynamic Tailwind classes using template literals
(e.g., text-${category.color}-400 and hover:border-${category.color}-500/30) are
not safe for JIT purging; create a static mapping object (e.g., colorClasses = {
cyan: { icon: 'text-cyan-400', hoverBorder: 'hover:border-cyan-500/30' },
purple: {...}, yellow: {...}, green: {...} }) and replace the template literals:
use category.icon with className={colorClasses[category.color].icon} and build
the button className by including colorClasses[category.color].hoverBorder (and
any other static pieces) instead of the template strings; mirror the approach
used in structured.tsx to ensure Tailwind picks up the classes.
🧹 Nitpick comments (11)
packages/typescript/smoke-tests/e2e/tests/tools/approval-flow.spec.ts (3)

1-1: Import statement can be improved per ESLint rules.

The static analysis flags import sorting and inline type specifier style. Consider using a top-level type import.

🔧 Suggested fix
-import { test, expect, type Page } from '@playwright/test'
+import type { Page } from '@playwright/test'
+import { expect, test } from '@playwright/test'

18-46: Helper functions are duplicated across multiple test spec files.

The selectScenario, waitForTestComplete, getMetadata, and getEventLog helpers are nearly identical across approval-flow.spec.ts, client-tool.spec.ts, and server-client-sequence.spec.ts. Consider extracting these to a shared test utilities module to reduce maintenance burden.


119-124: Empty catch block silently swallows errors.

The catch block returns an empty array without any indication that parsing failed. While acceptable for test helpers, consider logging a warning for debugging purposes.

🔧 Optional improvement
     try {
       return JSON.parse(el.textContent || '[]')
     } catch {
+      console.warn('Failed to parse event-log-json')
       return []
     }
packages/typescript/smoke-tests/adapters/src/llm-simulator.ts (2)

1-6: Import sorting can be improved per ESLint rules.

🔧 Suggested fix
 import type {
+  ChatOptions,
   StreamChunk,
-  ChatOptions,
   StructuredOutputOptions,
   StructuredOutputResult,
 } from '@tanstack/ai'

302-389: Complex but necessary iteration detection logic.

The determineIterationFromMessages method handles multiple message formats correctly:

  • Server tool results (role: 'tool')
  • UIMessage format (parts with output or approval.approved)
  • ModelMessage format (toolCalls with approval info)

The logic is well-documented with inline comments explaining each case. Consider adding a unit test specifically for this method to guard against regressions.

packages/typescript/smoke-tests/e2e/src/routes/api.tools-test.ts (1)

9-12: Import statement can be improved per ESLint rules.

🔧 Suggested fix
+import type { SimulatorScript } from '@tanstack/tests-adapters'
 import {
   createLLMSimulator,
-  type SimulatorScript,
 } from '@tanstack/tests-adapters'
packages/typescript/smoke-tests/e2e/tests/tools/server-client-sequence.spec.ts (1)

1-1: Import statement can be improved per ESLint rules.

🔧 Suggested fix
-import { test, expect, type Page } from '@playwright/test'
+import type { Page } from '@playwright/test'
+import { expect, test } from '@playwright/test'
packages/typescript/smoke-tests/e2e/src/routes/tools-test.tsx (1)

1-5: Import statement can be improved per ESLint rules.

🔧 Suggested fix
-import { useState, useCallback, useRef, useEffect } from 'react'
+import { useCallback, useEffect, useRef, useState } from 'react'
 import { createFileRoute } from '@tanstack/react-router'
-import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
+import { fetchServerSentEvents, useChat } from '@tanstack/ai-react'
packages/typescript/smoke-tests/e2e/tests/tools/client-tool.spec.ts (1)

1-1: Import statement can be improved per ESLint rules.

🔧 Suggested fix
-import { test, expect, type Page } from '@playwright/test'
+import type { Page } from '@playwright/test'
+import { expect, test } from '@playwright/test'
packages/typescript/smoke-tests/adapters/src/tests/tools/server-tool.test.ts (1)

7-16: Extract collectChunks into a shared test helper.

This helper is repeated across multiple tool test files; consider a shared utility to keep the test suite DRY.

testing/panel/src/routes/simulator.tsx (1)

55-126: Rename TOOL_TEMPLATES to camelCase.

Keeping variable names in camelCase improves consistency and aligns with the style rule.

♻️ Suggested rename
-const TOOL_TEMPLATES = [
+const toolTemplates = [
   {
     category: 'Server Tools',
     icon: Server,
     color: 'cyan',
     tools: [
       {
         name: 'Server Tool',
         template: 'serverTool({ text: "hello from server" })',
         description: 'Executes on server only',
       },
@@
-        {TOOL_TEMPLATES.map((category) => (
+        {toolTemplates.map((category) => (
As per coding guidelines: Use camelCase for function and variable names throughout the codebase.

Also applies to: 485-505


for (const message of this.messages) {
// todo remove any and fix this
// Check for UIMessage format (parts array)

Choose a reason for hiding this comment

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

Maybe keep TODO about removing any at some point?

Copy link
Contributor

@AlemTuzlak AlemTuzlak Feb 2, 2026

Choose a reason for hiding this comment

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

This seems to be completely deprecated and yet in place due to the any cast, I think this should be completely removed @jherr

Copy link
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: 5

🤖 Fix all issues with AI agents
In `@packages/typescript/ai-client/src/chat-client.ts`:
- Around line 384-397: Add a brief clarifying comment above the block that
checks streamCompletedSuccessfully to explain that lastPart is read from
this.processor.getMessages() which may have been updated by nested streams
(e.g., via drainPostStreamActions), so lastPart reflects the current message
state rather than the original stream's final state and therefore prevents
triggering a continuation when nested streams produced text; reference the
surrounding symbols like streamCompletedSuccessfully,
this.processor.getMessages(), lastPart, shouldAutoSend(), and
checkForContinuation() to show the intent.

In `@packages/typescript/smoke-tests/adapters/src/llm-simulator.ts`:
- Around line 1-6: Import members in the type-only import are not alphabetized;
reorder the imported type names in the import statement in llm-simulator.ts so
they follow alphabetical order (ChatOptions, StreamChunk,
StructuredOutputOptions, StructuredOutputResult) to satisfy ESLint's member
ordering rule.

In `@packages/typescript/smoke-tests/e2e/src/routes/tools-test.tsx`:
- Around line 1-5: Alphabetize the named import specifiers for each import
statement to satisfy the sort-imports rule: reorder "useState, useCallback,
useRef, useEffect" to "useCallback, useEffect, useRef, useState" in the React
import, reorder "useChat, fetchServerSentEvents" to "fetchServerSentEvents,
useChat" in the `@tanstack/ai-react` import, and ensure other single-name imports
like "toolDefinition" (from `@tanstack/ai`) and "z" (from zod) remain unchanged;
keep the import lines themselves as-is but update the member ordering for a
lint-clean result.
- Around line 597-617: Keep using dangerouslySetInnerHTML for the two script
tags but escape any "<" in the JSON to prevent closing </script> injection: wrap
the JSON.stringify(...) results for the script with .replace(/</g, '\\u003c') so
the event-log script (using toolEvents) and the tool-calls script (using
toolCalls.map(...) producing id/name/state/hasOutput) both output safe JSON
strings while remaining parseable.

In `@testing/panel/test-results/.last-run.json`:
- Around line 2-7: The committed .last-run.json is an auto-generated Playwright
test state file and must be ignored; add ".last-run.json" to .gitignore (either
the repo root or the test panel folder's .gitignore) and remove the tracked
.last-run.json from the commit by removing it from git's index (e.g., git rm
--cached <the .last-run.json>), then commit the .gitignore update and the
removal so the file is no longer tracked.
🧹 Nitpick comments (7)
testing/panel/tests/tool-flow.spec.ts (1)

158-161: Assertion always passes — provides no validation.

The assertion expect(available.length).toBeGreaterThanOrEqual(0) is always true since array length cannot be negative. If the intent is to verify that at least one tool-capable provider is available for testing, the assertion should be toBeGreaterThan(0).

♻️ Proposed fix
 test('at least one tool-capable provider should be available', async () => {
   const available = getToolCapableProviders()
-  expect(available.length).toBeGreaterThanOrEqual(0)
+  expect(available.length).toBeGreaterThan(0)
 })
testing/panel/playwright-report/index.html (1)

23428-23430: Exclude Playwright reports from version control.

This generated Playwright test report (945 KB, base64-encoded ZIP) should not be committed to the repository. It creates significant bloat, produces unreadable binary diffs, and adds noise to the git history. Add playwright-report/ to .gitignore and configure your CI to store reports as artifacts instead.

packages/typescript/ai-client/src/connection-adapters.ts (1)

299-312: Consider aligning comment terminology with fetch adapters.

The signature change to accept both UIMessage and ModelMessage arrays is a backward-compatible widening that provides flexibility.

Minor inconsistency: Lines 307-308 reference "Server-side chat()" while the fetch adapters (lines 145-146, 244-245) reference "Server-side TextEngine" for conversion handling. If both paths ultimately use the same conversion mechanism, consider using consistent terminology across all adapters for clarity.

packages/typescript/ai/src/activities/chat/index.ts (1)

762-766: Consider adding a defensive check for part.id.

If part.id is undefined for any reason (malformed message), this would create an entry with key "undefined" in the map, which could cause subtle issues when looking up by actual tool call IDs.

🛡️ Optional defensive check
           // Extract client tool results (tools without approval that have output)
-          if (part.output !== undefined && !part.approval) {
+          if (part.id && part.output !== undefined && !part.approval) {
             clientToolResults.set(part.id, part.output)
           }
packages/typescript/smoke-tests/adapters/src/llm-simulator.ts (1)

398-519: Rename SimulatorScripts to camelCase to match naming guidelines.

SimulatorScripts is a variable export; prefer camelCase to align with the project’s naming rule.

♻️ Suggested rename (remember to update usages)
-export const SimulatorScripts = {
+export const simulatorScripts = {
   /**
    * Script for a single server tool call
    */
   singleServerTool(
packages/typescript/smoke-tests/e2e/src/routes/tools-test.tsx (2)

27-95: Add .server() implementations to keep tools isomorphic.

Guidelines call for dual-environment tool definitions; these tools are currently client-only. Consider adding a server stub (or a real server implementation) to avoid divergence.

♻️ Suggested pattern (adjust chaining order per toolDefinition docs)
   const showNotificationTool = toolDefinition({
     name: 'show_notification',
     description: 'Show a notification to the user',
     inputSchema: z.object({
       message: z.string(),
       type: z.enum(['info', 'warning', 'error']),
     }),
     outputSchema: z.object({
       displayed: z.boolean(),
       timestamp: z.number(),
     }),
-  }).client(async (args) => {
+  })
+    .server(async () => {
+      throw new Error('show_notification is client-only in tools-test')
+    })
+    .client(async (args) => {
       addEvent({
         type: 'execution-start',
         toolName: 'show_notification',
         details: args.message,
       })

99-140: Rename SCENARIOS to camelCase.

The codebase guideline calls for camelCase variable names.

♻️ Suggested rename
-const SCENARIOS = [
+const scenarios = [
   { id: 'text-only', label: 'Text Only (No Tools)', category: 'basic' },
   { id: 'server-tool-single', label: 'Single Server Tool', category: 'basic' },
   { id: 'client-tool-single', label: 'Single Client Tool', category: 'basic' },
-          {SCENARIOS.filter((s) => s.category === 'basic').map((s) => (
+          {scenarios.filter((s) => s.category === 'basic').map((s) => (
             <option key={s.id} value={s.id}>
               {s.label}
             </option>
           ))}
-          {SCENARIOS.filter((s) => s.category === 'race').map((s) => (
+          {scenarios.filter((s) => s.category === 'race').map((s) => (
             <option key={s.id} value={s.id}>
               {s.label}
             </option>
           ))}

Comment on lines +384 to +397

// Continue conversation if the stream ended with a tool result (server tool completed)
if (streamCompletedSuccessfully) {
const messages = this.processor.getMessages()
const lastPart = messages.at(-1)?.parts.at(-1)

if (lastPart?.type === 'tool-result' && this.shouldAutoSend()) {
try {
await this.checkForContinuation()
} catch (error) {
console.error('Failed to continue flow after tool result:', error)
}
}
}
Copy link
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, let's examine the file structure and find the chat-client.ts file
find . -name "chat-client.ts" -type f

Repository: TanStack/ai

Length of output: 105


🏁 Script executed:

# Read the specific lines mentioned in the review (384-397)
sed -n '370,420p' packages/typescript/ai-client/src/chat-client.ts | cat -n

Repository: TanStack/ai

Length of output: 1896


🏁 Script executed:

# Look at the drainPostStreamActions method
rg -n "drainPostStreamActions" packages/typescript/ai-client/src/chat-client.ts -A 20

Repository: TanStack/ai

Length of output: 1426


🏁 Script executed:

# Look at the checkForContinuation method
rg -n "checkForContinuation" packages/typescript/ai-client/src/chat-client.ts -A 15

Repository: TanStack/ai

Length of output: 2180


🏁 Script executed:

# Check for any tests related to tool continuation and nested streams
find . -name "*.test.ts" -o -name "*.spec.ts" | xargs rg -l "tool.*result|continuation" | head -5

Repository: TanStack/ai

Length of output: 444


🏁 Script executed:

# Check the server-client-sequence test for nested tool scenarios
head -100 packages/typescript/smoke-tests/e2e/tests/tools/server-client-sequence.spec.ts

Repository: TanStack/ai

Length of output: 3195


🏁 Script executed:

# Look for tests that involve multiple tool calls or nested tool scenarios
rg -n "tool.*result|nested|continuation" packages/typescript/smoke-tests/e2e/tests/tools/server-client-sequence.spec.ts -B 2 -A 5

Repository: TanStack/ai

Length of output: 702


🏁 Script executed:

# Check if there are any comments in chat-client.ts explaining the streamCompletedSuccessfully logic
rg -n "streamCompletedSuccessfully" packages/typescript/ai-client/src/chat-client.ts -B 5 -A 2

Repository: TanStack/ai

Length of output: 993


🏁 Script executed:

# Look at the broader context where streamCompletedSuccessfully is set
sed -n '300,400p' packages/typescript/ai-client/src/chat-client.ts | cat -n

Repository: TanStack/ai

Length of output: 3857


Consider adding a clarifying comment about state synchronization after nested streams.

The implementation correctly handles nested streams from drainPostStreamActions(): streamCompletedSuccessfully reflects the original stream's completion, while lastPart fetches the current message state (potentially updated by nested streams). This prevents spurious continuations when nested streams produce text responses.

Adding a comment like the following would clarify this pattern for future maintainers:

// Note: lastPart reflects the current message state (potentially updated by
// drainPostStreamActions), not the original stream's final state.
// If nested streams produced a text response, lastPart won't be 'tool-result'.
🤖 Prompt for AI Agents
In `@packages/typescript/ai-client/src/chat-client.ts` around lines 384 - 397, Add
a brief clarifying comment above the block that checks
streamCompletedSuccessfully to explain that lastPart is read from
this.processor.getMessages() which may have been updated by nested streams
(e.g., via drainPostStreamActions), so lastPart reflects the current message
state rather than the original stream's final state and therefore prevents
triggering a continuation when nested streams produced text; reference the
surrounding symbols like streamCompletedSuccessfully,
this.processor.getMessages(), lastPart, shouldAutoSend(), and
checkForContinuation() to show the intent.

Comment on lines +1 to +6
import type {
StreamChunk,
ChatOptions,
StructuredOutputOptions,
StructuredOutputResult,
} from '@tanstack/ai'
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix import member ordering to satisfy lint rules.

ESLint expects alphabetized members in the type-only import.

♻️ Suggested fix
-import type {
-  StreamChunk,
-  ChatOptions,
-  StructuredOutputOptions,
-  StructuredOutputResult,
-} from '@tanstack/ai'
+import type {
+  ChatOptions,
+  StreamChunk,
+  StructuredOutputOptions,
+  StructuredOutputResult,
+} from '@tanstack/ai'
📝 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
import type {
StreamChunk,
ChatOptions,
StructuredOutputOptions,
StructuredOutputResult,
} from '@tanstack/ai'
import type {
ChatOptions,
StreamChunk,
StructuredOutputOptions,
StructuredOutputResult,
} from '@tanstack/ai'
🧰 Tools
🪛 ESLint

[error] 3-3: Member 'ChatOptions' of the import declaration should be sorted alphabetically.

(sort-imports)

🤖 Prompt for AI Agents
In `@packages/typescript/smoke-tests/adapters/src/llm-simulator.ts` around lines 1
- 6, Import members in the type-only import are not alphabetized; reorder the
imported type names in the import statement in llm-simulator.ts so they follow
alphabetical order (ChatOptions, StreamChunk, StructuredOutputOptions,
StructuredOutputResult) to satisfy ESLint's member ordering rule.

Comment on lines +1 to +5
import { useState, useCallback, useRef, useEffect } from 'react'
import { createFileRoute } from '@tanstack/react-router'
import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
import { toolDefinition } from '@tanstack/ai'
import { z } from 'zod'
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Sort import members to satisfy lint rules.

This aligns with the configured sort-imports checks.

♻️ Suggested fix
-import { useState, useCallback, useRef, useEffect } from 'react'
+import { useCallback, useEffect, useRef, useState } from 'react'
-import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
+import { fetchServerSentEvents, useChat } from '@tanstack/ai-react'
📝 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
import { useState, useCallback, useRef, useEffect } from 'react'
import { createFileRoute } from '@tanstack/react-router'
import { useChat, fetchServerSentEvents } from '@tanstack/ai-react'
import { toolDefinition } from '@tanstack/ai'
import { z } from 'zod'
import { useCallback, useEffect, useRef, useState } from 'react'
import { createFileRoute } from '@tanstack/react-router'
import { fetchServerSentEvents, useChat } from '@tanstack/ai-react'
import { toolDefinition } from '@tanstack/ai'
import { z } from 'zod'
🧰 Tools
🪛 ESLint

[error] 1-1: Member 'useCallback' of the import declaration should be sorted alphabetically.

(sort-imports)


[error] 3-3: Member 'fetchServerSentEvents' of the import declaration should be sorted alphabetically.

(sort-imports)

🤖 Prompt for AI Agents
In `@packages/typescript/smoke-tests/e2e/src/routes/tools-test.tsx` around lines 1
- 5, Alphabetize the named import specifiers for each import statement to
satisfy the sort-imports rule: reorder "useState, useCallback, useRef,
useEffect" to "useCallback, useEffect, useRef, useState" in the React import,
reorder "useChat, fetchServerSentEvents" to "fetchServerSentEvents, useChat" in
the `@tanstack/ai-react` import, and ensure other single-name imports like
"toolDefinition" (from `@tanstack/ai`) and "z" (from zod) remain unchanged; keep
the import lines themselves as-is but update the member ordering for a
lint-clean result.

Comment on lines +597 to +617
<script
id="event-log-json"
type="application/json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(toolEvents) }}
/>

{/* Tool calls as JSON for easy parsing in tests */}
<script
id="tool-calls-json"
type="application/json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(
toolCalls.map((tc) => ({
id: tc.id,
name: tc.name,
state: tc.state,
hasOutput: !!tc.output,
})),
),
}}
/>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n packages/typescript/smoke-tests/e2e/src/routes/tools-test.tsx | sed -n '590,625p'

Repository: TanStack/ai

Length of output: 1169


🌐 Web query:

React text children script tag type application/json escaping JSON parsing

💡 Result:

When you put JSON as a React text child inside a <script type="application/json"> (or application/ld+json) tag, React will HTML-escape it during SSR/HTML rendering, so quotes become &quot;, etc.—which breaks consumers that expect raw JSON text (e.g., JSON-LD parsers). The common fix is to inject the raw string with dangerouslySetInnerHTML. [1]

Safe pattern (raw JSON text, plus XSS-safe escaping)

const data = { /* possibly user-influenced data */ };

<script
  type="application/json"
  dangerouslySetInnerHTML={{
    __html: JSON.stringify(data).replace(/</g, '\\u003c'),
  }}
/>

Next.js explicitly recommends escaping < like this (or using a library such as serialize-javascript) because JSON.stringify alone does not prevent </script>-based injection. [2]

Reading it back

const el = document.getElementById('my-data');
const obj = JSON.parse(el.textContent);

Gotchas

  • Don’t set both children and dangerouslySetInnerHTML on the same element. [3]
  • If any fields can contain untrusted text, treat this as an XSS boundary: escape as above (at minimum) or use a hardened serializer. [2][4]

Sources: Next.js JSON-LD guidance (including the replace(/</g, '\\u003c') mitigation) [2], Next.js metadata JSON-LD example using dangerouslySetInnerHTML [1], React <script> docs / constraints [3], SSR/script-tag XSS discussion in React security notes [4].

[1] (nextjs.org)
[2] (nextjs.org)
[3] (react.dev)
[4] (0xn3va.gitbook.io)

Citations:


🏁 Script executed:

grep -n "const toolEvents\|let toolEvents\|toolEvents =" packages/typescript/smoke-tests/e2e/src/routes/tools-test.tsx | head -20

Repository: TanStack/ai

Length of output: 37


🏁 Script executed:

grep -n "const toolCalls\|let toolCalls\|toolCalls =" packages/typescript/smoke-tests/e2e/src/routes/tools-test.tsx | head -20

Repository: TanStack/ai

Length of output: 104


🏁 Script executed:

cat -n packages/typescript/smoke-tests/e2e/src/routes/tools-test.tsx | sed -n '200,240p'

Repository: TanStack/ai

Length of output: 1433


🏁 Script executed:

cat -n packages/typescript/smoke-tests/e2e/src/routes/tools-test.tsx | sed -n '100,160p'

Repository: TanStack/ai

Length of output: 2610


🏁 Script executed:

cat -n packages/typescript/smoke-tests/e2e/src/routes/tools-test.tsx | sed -n '160,200p'

Repository: TanStack/ai

Length of output: 1681


🏁 Script executed:

grep -n "createTrackedTools\|ToolEvent" packages/typescript/smoke-tests/e2e/src/routes/tools-test.tsx | head -30

Repository: TanStack/ai

Length of output: 501


🏁 Script executed:

cat -n packages/typescript/smoke-tests/e2e/src/routes/tools-test.tsx | sed -n '1,50p'

Repository: TanStack/ai

Length of output: 1719


Use replace(/</g, '\\u003c') to safely escape JSON in script tags—the proposed text-child approach breaks JSON parsing.

The security concern is valid: </script> sequences in toolEvents.details or similar fields could break out of the script tag. However, using direct text children (as suggested) will cause React to HTML-escape the entire JSON string, making it unparseable when read back.

The correct fix is to keep dangerouslySetInnerHTML but add escaping:

-dangerouslySetInnerHTML={{ __html: JSON.stringify(toolEvents) }}
+dangerouslySetInnerHTML={{ __html: JSON.stringify(toolEvents).replace(/</g, '\\u003c') }}

Apply the same escaping to both script tags' JSON.stringify() calls.

📝 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
<script
id="event-log-json"
type="application/json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(toolEvents) }}
/>
{/* Tool calls as JSON for easy parsing in tests */}
<script
id="tool-calls-json"
type="application/json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(
toolCalls.map((tc) => ({
id: tc.id,
name: tc.name,
state: tc.state,
hasOutput: !!tc.output,
})),
),
}}
/>
<script
id="event-log-json"
type="application/json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(toolEvents).replace(/</g, '\\u003c') }}
/>
{/* Tool calls as JSON for easy parsing in tests */}
<script
id="tool-calls-json"
type="application/json"
dangerouslySetInnerHTML={{
__html: JSON.stringify(
toolCalls.map((tc) => ({
id: tc.id,
name: tc.name,
state: tc.state,
hasOutput: !!tc.output,
})),
).replace(/</g, '\\u003c'),
}}
/>
🧰 Tools
🪛 ast-grep (0.40.5)

[warning] 599-599: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)


[warning] 606-606: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🪛 Biome (2.3.13)

[error] 600-600: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)


[error] 607-608: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

🤖 Prompt for AI Agents
In `@packages/typescript/smoke-tests/e2e/src/routes/tools-test.tsx` around lines
597 - 617, Keep using dangerouslySetInnerHTML for the two script tags but escape
any "<" in the JSON to prevent closing </script> injection: wrap the
JSON.stringify(...) results for the script with .replace(/</g, '\\u003c') so the
event-log script (using toolEvents) and the tool-calls script (using
toolCalls.map(...) producing id/name/state/hasOutput) both output safe JSON
strings while remaining parseable.

Comment on lines +2 to +7
"status": "failed",
"failedTests": [
"e6d271510b4cd4631a47-390d9afc5f702f185ad7",
"e6d271510b4cd4631a47-abc15f00eeb116802b74",
"e6d271510b4cd4631a47-22ee07d931418c7e699e"
]
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if there's an existing .gitignore in the testing/panel directory
echo "=== Checking for .gitignore in testing/panel ==="
cat testing/panel/.gitignore 2>/dev/null || echo "No .gitignore found in testing/panel"

# Check root .gitignore for test-results patterns
echo ""
echo "=== Root .gitignore patterns ==="
grep -E "(test-results|playwright|\.last-run)" .gitignore 2>/dev/null || echo "No relevant patterns found"

# Check if the file exists and its current status
echo ""
echo "=== Checking file existence and git status ==="
ls -la testing/panel/test-results/.last-run.json 2>/dev/null || echo "File not found in working directory"
cd testing/panel && find . -name ".gitignore" -type f 2>/dev/null && echo "--- .gitignore files found in testing/panel ---" || echo "No .gitignore files in testing/panel"
cd - > /dev/null

Repository: TanStack/ai

Length of output: 387


🏁 Script executed:

#!/bin/bash
# Search for Playwright references in config files
echo "=== Playwright configuration and references ==="
find . -maxdepth 2 -name "playwright.config.*" -o -name "vitest.config.*" -o -name "package.json" | head -5
echo ""

# Check root package.json for test scripts
echo "=== Test scripts in package.json ==="
grep -A5 '"test' package.json 2>/dev/null | head -20
echo ""

# Look for any documentation about test-results
echo "=== References to test-results or .last-run in docs ==="
find . -type f \( -name "README*" -o -name "*.md" \) | xargs grep -l "test-results\|\.last-run" 2>/dev/null || echo "No doc references found"
echo ""

# Check .gitignore files in the entire repo to see patterns
echo "=== All .gitignore patterns related to test or build artifacts ==="
find . -name ".gitignore" -type f | xargs cat 2>/dev/null | grep -E "(test|build|dist|node_modules|coverage)" | head -20

Repository: TanStack/ai

Length of output: 1700


🏁 Script executed:

#!/bin/bash
# Search for Playwright in dependencies
echo "=== Playwright in package.json ==="
grep -i "playwright" package.json 2>/dev/null || echo "No Playwright found in root package.json"

echo ""
echo "=== Checking testing/panel directory ==="
ls -la testing/panel/ 2>/dev/null | head -20

echo ""
echo "=== If testing/panel/package.json exists ==="
cat testing/panel/package.json 2>/dev/null | head -50 || echo "No package.json in testing/panel"

echo ""
echo "=== Contents of .last-run.json ==="
cat testing/panel/test-results/.last-run.json 2>/dev/null

echo ""
echo "=== Search for .last-run.json references in codebase ==="
rg "\.last-run\.json" --type json --type ts --type js 2>/dev/null | head -10 || echo "No references found"

Repository: TanStack/ai

Length of output: 2838


Add .last-run.json to .gitignore and remove from the commit.

This file is auto-generated by Playwright to track test run state and should not be committed. The presence of "status": "failed" in the committed artifact is particularly problematic because:

  • It will cause merge conflicts when multiple developers run tests locally
  • It introduces machine-generated state into version control that should be ephemeral
  • The failed status in a committed artifact indicates test failures were recorded

Recommended fix: Add the following to the root .gitignore (or create testing/panel/.gitignore if preferred):

+# Playwright test artifacts
+testing/panel/test-results/
+testing/panel/playwright-report/

Then remove testing/panel/test-results/.last-run.json from this commit.

🤖 Prompt for AI Agents
In `@testing/panel/test-results/.last-run.json` around lines 2 - 7, The committed
.last-run.json is an auto-generated Playwright test state file and must be
ignored; add ".last-run.json" to .gitignore (either the repo root or the test
panel folder's .gitignore) and remove the tracked .last-run.json from the commit
by removing it from git's index (e.g., git rm --cached <the .last-run.json>),
then commit the .gitignore update and the removal so the file is no longer
tracked.

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.

3 participants