@@ -17,6 +17,7 @@ import { processUserContentMentions } from "../../mentions/processUserContentMen
1717import { MultiSearchReplaceDiffStrategy } from "../../diff/strategies/multi-search-replace"
1818import { MultiFileSearchReplaceDiffStrategy } from "../../diff/strategies/multi-file-search-replace"
1919import { EXPERIMENT_IDS } from "../../../shared/experiments"
20+ import { formatResponse } from "../../prompts/responses"
2021
2122// Mock delay before any imports that might use it
2223vi . mock ( "delay" , ( ) => ( {
@@ -1493,5 +1494,142 @@ describe("Cline", () => {
14931494 expect ( noModelTask . apiConfiguration . apiProvider ) . toBe ( "openai" )
14941495 } )
14951496 } )
1497+
1498+ describe ( "Ask mode conversational responses" , ( ) => {
1499+ it ( "should allow conversational responses in Ask mode without forcing tool use" , async ( ) => {
1500+ // Mock provider with Ask mode
1501+ const askModeProvider = {
1502+ ...mockProvider ,
1503+ getState : vi . fn ( ) . mockResolvedValue ( {
1504+ mode : "ask" ,
1505+ } ) ,
1506+ }
1507+
1508+ // Create task with history item that has ask mode
1509+ const askTask = new Task ( {
1510+ provider : askModeProvider ,
1511+ apiConfiguration : mockApiConfig ,
1512+ historyItem : {
1513+ id : "test-ask-task" ,
1514+ number : 1 ,
1515+ ts : Date . now ( ) ,
1516+ task : "What is TypeScript?" ,
1517+ tokensIn : 0 ,
1518+ tokensOut : 0 ,
1519+ cacheWrites : 0 ,
1520+ cacheReads : 0 ,
1521+ totalCost : 0 ,
1522+ mode : "ask" , // This sets the task mode
1523+ } ,
1524+ startTask : false ,
1525+ } )
1526+
1527+ // Mock the API stream response without tool use
1528+ const mockStream = {
1529+ async * [ Symbol . asyncIterator ] ( ) {
1530+ yield { type : "text" , text : "TypeScript is a typed superset of JavaScript..." }
1531+ } ,
1532+ async next ( ) {
1533+ return {
1534+ done : true ,
1535+ value : { type : "text" , text : "TypeScript is a typed superset of JavaScript..." } ,
1536+ }
1537+ } ,
1538+ async return ( ) {
1539+ return { done : true , value : undefined }
1540+ } ,
1541+ async throw ( e : any ) {
1542+ throw e
1543+ } ,
1544+ [ Symbol . asyncDispose ] : async ( ) => { } ,
1545+ } as AsyncGenerator < ApiStreamChunk >
1546+
1547+ vi . spyOn ( askTask . api , "createMessage" ) . mockReturnValue ( mockStream )
1548+
1549+ // Mock assistant message content without tool use
1550+ askTask . assistantMessageContent = [
1551+ {
1552+ type : "text" ,
1553+ content : "TypeScript is a typed superset of JavaScript..." ,
1554+ partial : false ,
1555+ } ,
1556+ ]
1557+
1558+ // Mock userMessageContentReady
1559+ askTask . userMessageContentReady = true
1560+
1561+ // Spy on recursivelyMakeClineRequests to check if it returns true (ends loop)
1562+ const recursiveSpy = vi . spyOn ( askTask , "recursivelyMakeClineRequests" )
1563+
1564+ // Execute the request
1565+ const result = await askTask . recursivelyMakeClineRequests ( [
1566+ { type : "text" , text : "What is TypeScript?" } ,
1567+ ] )
1568+
1569+ // Verify that the loop ends successfully without forcing tool use
1570+ expect ( result ) . toBe ( true )
1571+
1572+ // Verify that no "noToolsUsed" error was added
1573+ expect ( askTask . userMessageContent ) . not . toContainEqual (
1574+ expect . objectContaining ( {
1575+ type : "text" ,
1576+ text : expect . stringContaining ( "You did not use a tool" ) ,
1577+ } ) ,
1578+ )
1579+
1580+ // Verify consecutive mistake count was not incremented
1581+ expect ( askTask . consecutiveMistakeCount ) . toBe ( 0 )
1582+ } )
1583+
1584+ it ( "should still enforce tool use in non-Ask modes" , async ( ) => {
1585+ // Test the actual logic in initiateTaskLoop
1586+ const testUserContent = [ { type : "text" as const , text : "test" } ]
1587+
1588+ // Test code mode
1589+ const codeTask = new Task ( {
1590+ provider : mockProvider ,
1591+ apiConfiguration : mockApiConfig ,
1592+ historyItem : {
1593+ id : "test-code-task" ,
1594+ number : 2 ,
1595+ ts : Date . now ( ) ,
1596+ task : "Write a function" ,
1597+ tokensIn : 0 ,
1598+ tokensOut : 0 ,
1599+ cacheWrites : 0 ,
1600+ cacheReads : 0 ,
1601+ totalCost : 0 ,
1602+ mode : "code" ,
1603+ } ,
1604+ startTask : false ,
1605+ } )
1606+
1607+ // Directly test the logic from initiateTaskLoop
1608+ let nextUserContent = testUserContent
1609+ const didEndLoop = false // Simulating recursivelyMakeClineRequests returning false
1610+
1611+ if ( ! didEndLoop ) {
1612+ // This is the actual code from initiateTaskLoop
1613+ const currentMode = await codeTask . getTaskMode ( )
1614+ if ( currentMode === "ask" ) {
1615+ // In Ask mode, allow the conversation to end without forcing tool use
1616+ // This would break the loop
1617+ } else {
1618+ // For other modes, maintain the existing behavior
1619+ nextUserContent = [ { type : "text" as const , text : formatResponse . noToolsUsed ( ) } ]
1620+ codeTask . consecutiveMistakeCount ++
1621+ }
1622+ }
1623+
1624+ // Verify the behavior for code mode
1625+ expect ( nextUserContent ) . toContainEqual (
1626+ expect . objectContaining ( {
1627+ type : "text" ,
1628+ text : expect . stringContaining ( "You did not use a tool" ) ,
1629+ } ) ,
1630+ )
1631+ expect ( codeTask . consecutiveMistakeCount ) . toBe ( 1 )
1632+ } )
1633+ } )
14961634 } )
14971635} )
0 commit comments