Skip to content

Commit 0bcb9c6

Browse files
committed
Add submitUserMessage to Task
1 parent 3ee6072 commit 0bcb9c6

File tree

3 files changed

+147
-0
lines changed

3 files changed

+147
-0
lines changed

packages/types/src/task.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export interface TaskLike {
7070
off<K extends keyof TaskEvents>(event: K, listener: (...args: TaskEvents[K]) => void | Promise<void>): this
7171

7272
setMessageResponse(text: string, images?: string[]): void
73+
submitUserMessage(text: string, images?: string[]): void
7374
}
7475

7576
export type TaskEvents = {

src/core/task/Task.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -740,6 +740,32 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
740740
this.askResponseImages = images
741741
}
742742

743+
public submitUserMessage(text: string, images?: string[]): void {
744+
try {
745+
const trimmed = (text ?? "").trim()
746+
const imgs = images ?? []
747+
748+
if (!trimmed && imgs.length === 0) {
749+
return
750+
}
751+
752+
const provider = this.providerRef.deref()
753+
if (!provider) {
754+
console.error("[Task#submitUserMessage] Provider reference lost")
755+
return
756+
}
757+
758+
void provider.postMessageToWebview({
759+
type: "invoke",
760+
invoke: "sendMessage",
761+
text: trimmed,
762+
images: imgs,
763+
})
764+
} catch (error) {
765+
console.error("[Task#submitUserMessage] Failed to submit user message:", error)
766+
}
767+
}
768+
743769
async handleTerminalOperation(terminalOperation: "continue" | "abort") {
744770
if (terminalOperation === "continue") {
745771
this.terminalProcess?.continue()

src/core/task/__tests__/Task.spec.ts

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1493,5 +1493,125 @@ describe("Cline", () => {
14931493
expect(noModelTask.apiConfiguration.apiProvider).toBe("openai")
14941494
})
14951495
})
1496+
1497+
describe("submitUserMessage", () => {
1498+
it("should always route through webview sendMessage invoke", async () => {
1499+
const task = new Task({
1500+
provider: mockProvider,
1501+
apiConfiguration: mockApiConfig,
1502+
task: "initial task",
1503+
startTask: false,
1504+
})
1505+
1506+
// Set up some existing messages to simulate an ongoing conversation
1507+
task.clineMessages = [
1508+
{
1509+
ts: Date.now(),
1510+
type: "say",
1511+
say: "text",
1512+
text: "Initial message",
1513+
},
1514+
]
1515+
1516+
// Call submitUserMessage
1517+
task.submitUserMessage("test message", ["image1.png"])
1518+
1519+
// Verify postMessageToWebview was called with sendMessage invoke
1520+
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
1521+
type: "invoke",
1522+
invoke: "sendMessage",
1523+
text: "test message",
1524+
images: ["image1.png"],
1525+
})
1526+
})
1527+
1528+
it("should handle empty messages gracefully", async () => {
1529+
const task = new Task({
1530+
provider: mockProvider,
1531+
apiConfiguration: mockApiConfig,
1532+
task: "initial task",
1533+
startTask: false,
1534+
})
1535+
1536+
// Call with empty text and no images
1537+
task.submitUserMessage("", [])
1538+
1539+
// Should not call postMessageToWebview for empty messages
1540+
expect(mockProvider.postMessageToWebview).not.toHaveBeenCalled()
1541+
1542+
// Call with whitespace only
1543+
task.submitUserMessage(" ", [])
1544+
expect(mockProvider.postMessageToWebview).not.toHaveBeenCalled()
1545+
})
1546+
1547+
it("should route through webview for both new and existing tasks", async () => {
1548+
const task = new Task({
1549+
provider: mockProvider,
1550+
apiConfiguration: mockApiConfig,
1551+
task: "initial task",
1552+
startTask: false,
1553+
})
1554+
1555+
// Test with no messages (new task scenario)
1556+
task.clineMessages = []
1557+
task.submitUserMessage("new task", ["image1.png"])
1558+
1559+
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
1560+
type: "invoke",
1561+
invoke: "sendMessage",
1562+
text: "new task",
1563+
images: ["image1.png"],
1564+
})
1565+
1566+
// Clear mock
1567+
mockProvider.postMessageToWebview.mockClear()
1568+
1569+
// Test with existing messages (ongoing task scenario)
1570+
task.clineMessages = [
1571+
{
1572+
ts: Date.now(),
1573+
type: "say",
1574+
say: "text",
1575+
text: "Initial message",
1576+
},
1577+
]
1578+
task.submitUserMessage("follow-up message", ["image2.png"])
1579+
1580+
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
1581+
type: "invoke",
1582+
invoke: "sendMessage",
1583+
text: "follow-up message",
1584+
images: ["image2.png"],
1585+
})
1586+
})
1587+
1588+
it("should handle undefined provider gracefully", async () => {
1589+
const task = new Task({
1590+
provider: mockProvider,
1591+
apiConfiguration: mockApiConfig,
1592+
task: "initial task",
1593+
startTask: false,
1594+
})
1595+
1596+
// Simulate weakref returning undefined
1597+
Object.defineProperty(task, "providerRef", {
1598+
value: { deref: () => undefined },
1599+
writable: false,
1600+
configurable: true,
1601+
})
1602+
1603+
// Spy on console.error to verify error is logged
1604+
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
1605+
1606+
// Should log error but not throw
1607+
task.submitUserMessage("test message")
1608+
1609+
expect(consoleErrorSpy).toHaveBeenCalledWith("[Task#submitUserMessage] Provider reference lost")
1610+
expect(mockProvider.postMessageToWebview).not.toHaveBeenCalled()
1611+
1612+
// Restore console.error
1613+
consoleErrorSpy.mockRestore()
1614+
})
1615+
})
14961616
})
14971617
})

0 commit comments

Comments
 (0)