Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/types/src/task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export interface TaskLike {
off<K extends keyof TaskEvents>(event: K, listener: (...args: TaskEvents[K]) => void | Promise<void>): this

setMessageResponse(text: string, images?: string[]): void
submitUserMessage(text: string, images?: string[]): void
}

export type TaskEvents = {
Expand Down
26 changes: 26 additions & 0 deletions src/core/task/Task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -740,6 +740,32 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
this.askResponseImages = images
}

public submitUserMessage(text: string, images?: string[]): void {
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider adding JSDoc documentation for this public method to maintain consistency with other public methods and improve IDE tooltips:

Suggested change
public submitUserMessage(text: string, images?: string[]): void {
/**
* Submits a user message to the webview.
* @param text - The message text to submit
* @param images - Optional array of image paths to include with the message
*/
public submitUserMessage(text: string, images?: string[]): void {

try {
const trimmed = (text ?? "").trim()
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider simplifying the null handling here. You're using text ?? "" but then checking !trimmed. This could be simplified:

Suggested change
const trimmed = (text ?? "").trim()
const trimmed = text?.trim() || ""
const imgs = images || []
if (!trimmed && imgs.length === 0) {
return
}

This would be more consistent with how you handle the images parameter.

const imgs = images ?? []

if (!trimmed && imgs.length === 0) {
return
}

const provider = this.providerRef.deref()
if (!provider) {
console.error("[Task#submitUserMessage] Provider reference lost")
return
}

void provider.postMessageToWebview({
type: "invoke",
invoke: "sendMessage",
text: trimmed,
images: imgs,
})
} catch (error) {
console.error("[Task#submitUserMessage] Failed to submit user message:", error)
}
}

async handleTerminalOperation(terminalOperation: "continue" | "abort") {
if (terminalOperation === "continue") {
this.terminalProcess?.continue()
Expand Down
120 changes: 120 additions & 0 deletions src/core/task/__tests__/Task.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1493,5 +1493,125 @@ describe("Cline", () => {
expect(noModelTask.apiConfiguration.apiProvider).toBe("openai")
})
})

Copy link
Contributor

Choose a reason for hiding this comment

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

Good test coverage! Consider adding one more test case for completeness - testing the scenario where only images are provided without text:

it("should handle images-only messages", async () => {
  const task = new Task({
    provider: mockProvider,
    apiConfiguration: mockApiConfig,
    task: "initial task",
    startTask: false,
  })

  // Call with images but no text
  task.submitUserMessage("", ["image1.png", "image2.png"])

  // Should not call postMessageToWebview since text is empty
  expect(mockProvider.postMessageToWebview).not.toHaveBeenCalled()
})

This would ensure the edge case is explicitly tested, even though the current implementation handles it correctly.

describe("submitUserMessage", () => {
it("should always route through webview sendMessage invoke", async () => {
const task = new Task({
provider: mockProvider,
apiConfiguration: mockApiConfig,
task: "initial task",
startTask: false,
})

// Set up some existing messages to simulate an ongoing conversation
task.clineMessages = [
{
ts: Date.now(),
type: "say",
say: "text",
text: "Initial message",
},
]

// Call submitUserMessage
task.submitUserMessage("test message", ["image1.png"])

// Verify postMessageToWebview was called with sendMessage invoke
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "invoke",
invoke: "sendMessage",
text: "test message",
images: ["image1.png"],
})
})

it("should handle empty messages gracefully", async () => {
const task = new Task({
provider: mockProvider,
apiConfiguration: mockApiConfig,
task: "initial task",
startTask: false,
})

// Call with empty text and no images
task.submitUserMessage("", [])

// Should not call postMessageToWebview for empty messages
expect(mockProvider.postMessageToWebview).not.toHaveBeenCalled()

// Call with whitespace only
task.submitUserMessage(" ", [])
expect(mockProvider.postMessageToWebview).not.toHaveBeenCalled()
})

it("should route through webview for both new and existing tasks", async () => {
const task = new Task({
provider: mockProvider,
apiConfiguration: mockApiConfig,
task: "initial task",
startTask: false,
})

// Test with no messages (new task scenario)
task.clineMessages = []
task.submitUserMessage("new task", ["image1.png"])

expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "invoke",
invoke: "sendMessage",
text: "new task",
images: ["image1.png"],
})

// Clear mock
mockProvider.postMessageToWebview.mockClear()

// Test with existing messages (ongoing task scenario)
task.clineMessages = [
{
ts: Date.now(),
type: "say",
say: "text",
text: "Initial message",
},
]
task.submitUserMessage("follow-up message", ["image2.png"])

expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "invoke",
invoke: "sendMessage",
text: "follow-up message",
images: ["image2.png"],
})
})

it("should handle undefined provider gracefully", async () => {
const task = new Task({
provider: mockProvider,
apiConfiguration: mockApiConfig,
task: "initial task",
startTask: false,
})

// Simulate weakref returning undefined
Object.defineProperty(task, "providerRef", {
value: { deref: () => undefined },
writable: false,
configurable: true,
})

// Spy on console.error to verify error is logged
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {})

// Should log error but not throw
task.submitUserMessage("test message")

expect(consoleErrorSpy).toHaveBeenCalledWith("[Task#submitUserMessage] Provider reference lost")
expect(mockProvider.postMessageToWebview).not.toHaveBeenCalled()

// Restore console.error
consoleErrorSpy.mockRestore()
})
})
})
})
Loading