Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
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/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export const historyItemSchema = z.object({
number: z.number(),
ts: z.number(),
task: z.string(),
title: z.string().optional(), // User-defined title for the task
tokensIn: z.number(),
tokensOut: z.number(),
cacheWrites: z.number().optional(),
Expand Down
186 changes: 186 additions & 0 deletions src/core/webview/__tests__/webviewMessageHandler.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const mockClineProvider = {
getCurrentTask: vi.fn(),
getTaskWithId: vi.fn(),
createTaskWithHistoryItem: vi.fn(),
updateTaskHistory: vi.fn(),
} as unknown as ClineProvider

import { t } from "../../../i18n"
Expand Down Expand Up @@ -708,3 +709,188 @@ describe("webviewMessageHandler - mcpEnabled", () => {
expect(mockClineProvider.postStateToWebview).toHaveBeenCalledTimes(1)
})
})

describe("webviewMessageHandler - updateTaskTitle", () => {
beforeEach(() => {
vi.clearAllMocks()
// Mock getGlobalState to return a task history
vi.mocked(mockClineProvider.contextProxy.getValue).mockReturnValue([
{
id: "task-1",
ts: 123456789,
task: "Original task text",
title: "Original title",
},
{
id: "task-2",
ts: 987654321,
task: "Another task",
// No title
},
])
})

it("should update task title when task exists", async () => {
// Mock updateTaskHistory to succeed
vi.mocked(mockClineProvider.updateTaskHistory).mockResolvedValue([])

await webviewMessageHandler(mockClineProvider, {
type: "updateTaskTitle",
taskId: "task-1",
title: "New updated title",
})

// Verify updateTaskHistory was called with the updated task
expect(mockClineProvider.updateTaskHistory).toHaveBeenCalledWith({
id: "task-1",
ts: 123456789,
task: "Original task text",
title: "New updated title",
})

// Verify state was posted to webview
expect(mockClineProvider.postStateToWebview).toHaveBeenCalled()
})

it("should clear task title when empty string is provided", async () => {
vi.mocked(mockClineProvider.updateTaskHistory).mockResolvedValue([])

await webviewMessageHandler(mockClineProvider, {
type: "updateTaskTitle",
taskId: "task-1",
title: "",
})

// Verify updateTaskHistory was called with undefined title
expect(mockClineProvider.updateTaskHistory).toHaveBeenCalledWith({
id: "task-1",
ts: 123456789,
task: "Original task text",
title: undefined,
})

expect(mockClineProvider.postStateToWebview).toHaveBeenCalled()
})

it("should add title to task that didn't have one", async () => {
vi.mocked(mockClineProvider.updateTaskHistory).mockResolvedValue([])

await webviewMessageHandler(mockClineProvider, {
type: "updateTaskTitle",
taskId: "task-2",
title: "Brand new title",
})

// Verify updateTaskHistory was called with the new title
expect(mockClineProvider.updateTaskHistory).toHaveBeenCalledWith({
id: "task-2",
ts: 987654321,
task: "Another task",
title: "Brand new title",
})

expect(mockClineProvider.postStateToWebview).toHaveBeenCalled()
})

it("should not update when task is not found", async () => {
await webviewMessageHandler(mockClineProvider, {
type: "updateTaskTitle",
taskId: "non-existent-task",
title: "Some title",
})

// Verify updateTaskHistory was NOT called
expect(mockClineProvider.updateTaskHistory).not.toHaveBeenCalled()

// State should not be posted either
expect(mockClineProvider.postStateToWebview).not.toHaveBeenCalled()
})

it("should not update when taskId is missing", async () => {
await webviewMessageHandler(mockClineProvider, {
type: "updateTaskTitle",
// No taskId provided
title: "Some title",
})

// Verify updateTaskHistory was NOT called
expect(mockClineProvider.updateTaskHistory).not.toHaveBeenCalled()

// State should not be posted either
expect(mockClineProvider.postStateToWebview).not.toHaveBeenCalled()
})

it("should handle empty task history gracefully", async () => {
// Mock empty task history
vi.mocked(mockClineProvider.contextProxy.getValue).mockReturnValue(undefined)

await webviewMessageHandler(mockClineProvider, {
type: "updateTaskTitle",
taskId: "task-1",
title: "New title",
})

// Verify updateTaskHistory was NOT called
expect(mockClineProvider.updateTaskHistory).not.toHaveBeenCalled()

// State should not be posted either
expect(mockClineProvider.postStateToWebview).not.toHaveBeenCalled()
})

it("should handle null task history gracefully", async () => {
// Mock null task history
vi.mocked(mockClineProvider.contextProxy.getValue).mockReturnValue(null)

await webviewMessageHandler(mockClineProvider, {
type: "updateTaskTitle",
taskId: "task-1",
title: "New title",
})

// Verify updateTaskHistory was NOT called
expect(mockClineProvider.updateTaskHistory).not.toHaveBeenCalled()

// State should not be posted either
expect(mockClineProvider.postStateToWebview).not.toHaveBeenCalled()
})

it("should trim whitespace from title", async () => {
vi.mocked(mockClineProvider.updateTaskHistory).mockResolvedValue([])

await webviewMessageHandler(mockClineProvider, {
type: "updateTaskTitle",
taskId: "task-1",
title: " Trimmed Title ",
})

// Verify updateTaskHistory was called with trimmed title
expect(mockClineProvider.updateTaskHistory).toHaveBeenCalledWith({
id: "task-1",
ts: 123456789,
task: "Original task text",
title: "Trimmed Title",
})

expect(mockClineProvider.postStateToWebview).toHaveBeenCalled()
})

it("should handle whitespace-only title as empty", async () => {
vi.mocked(mockClineProvider.updateTaskHistory).mockResolvedValue([])

await webviewMessageHandler(mockClineProvider, {
type: "updateTaskTitle",
taskId: "task-1",
title: " ",
})

// Verify updateTaskHistory was called with undefined (cleared title)
expect(mockClineProvider.updateTaskHistory).toHaveBeenCalledWith({
id: "task-1",
ts: 123456789,
task: "Original task text",
title: undefined,
})

expect(mockClineProvider.postStateToWebview).toHaveBeenCalled()
})
})
29 changes: 29 additions & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3110,5 +3110,34 @@ export const webviewMessageHandler = async (
})
break
}
case "updateTaskTitle": {
// Handle task title update
if (message.taskId && message.title !== undefined) {
try {
// Get the current task history
const history = getGlobalState("taskHistory") ?? []
const taskItem = history.find((item) => item.id === message.taskId)

if (taskItem) {
// Update the title field - trim whitespace and set to undefined if empty
const trimmedTitle = message.title?.trim()
taskItem.title = trimmedTitle || undefined // Set to undefined if empty string

// Save the updated task item
await provider.updateTaskHistory(taskItem)

// Post updated state back to webview
await provider.postStateToWebview()
} else {
provider.log(`Task not found for title update: ${message.taskId}`)
vscode.window.showErrorMessage(t("common:errors.task_not_found"))
}
} catch (error) {
provider.log(`Error updating task title: ${error instanceof Error ? error.message : String(error)}`)
vscode.window.showErrorMessage(t("common:errors.update_task_title"))
}
}
break
}
}
}
2 changes: 2 additions & 0 deletions src/i18n/locales/en/common.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@
"error_deleting_message": "Error deleting message: {{error}}",
"error_editing_message": "Error editing message: {{error}}"
},
"task_not_found": "Task not found",
"update_task_title": "Failed to update task title",
"gemini": {
"generate_stream": "Gemini generate context stream error: {{error}}",
"generate_complete_prompt": "Gemini completion error: {{error}}",
Expand Down
3 changes: 3 additions & 0 deletions src/shared/ExtensionMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,11 @@ export interface ExtensionMessage {
| "insertTextIntoTextarea"
| "dismissedUpsells"
| "organizationSwitchResult"
| "taskTitleUpdated"
text?: string
payload?: any // Add a generic payload for now, can refine later
taskId?: string // For task title updates
title?: string // For task title updates
action?:
| "chatButtonClicked"
| "mcpButtonClicked"
Expand Down
3 changes: 3 additions & 0 deletions src/shared/WebviewMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export interface WebviewMessage {
| "showTaskWithId"
| "deleteTaskWithId"
| "exportTaskWithId"
| "updateTaskTitle"
| "importSettings"
| "exportSettings"
| "resetState"
Expand Down Expand Up @@ -235,6 +236,8 @@ export interface WebviewMessage {
disabled?: boolean
context?: string
dataUri?: string
taskId?: string // For task title updates
title?: string // For task title updates
askResponse?: ClineAskResponse
apiConfiguration?: ProviderSettings
images?: string[]
Expand Down
Loading
Loading