Skip to content
Open
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/global-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ export const globalSettingsSchema = z.object({
includeTaskHistoryInEnhance: z.boolean().optional(),
historyPreviewCollapsed: z.boolean().optional(),
reasoningBlockCollapsed: z.boolean().optional(),
taskTitlesEnabled: z.boolean().optional(),
profileThresholds: z.record(z.string(), z.number()).optional(),
hasOpenedModeSelector: z.boolean().optional(),
lastModeExportPath: z.string().optional(),
Expand Down
1 change: 1 addition & 0 deletions packages/types/src/history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export const historyItemSchema = z.object({
parentTaskId: z.string().optional(),
number: z.number(),
ts: z.number(),
title: z.string().optional(),
task: z.string(),
tokensIn: z.number(),
tokensOut: z.number(),
Expand Down
15 changes: 14 additions & 1 deletion src/core/webview/ClineProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1793,6 +1793,7 @@ export class ClineProvider
terminalCompressProgressBar,
historyPreviewCollapsed,
reasoningBlockCollapsed,
taskTitlesEnabled,
cloudUserInfo,
cloudIsAuthenticated,
sharingEnabled,
Expand Down Expand Up @@ -1866,6 +1867,7 @@ export class ClineProvider
taskHistory: (taskHistory || [])
.filter((item: HistoryItem) => item.ts && item.task)
.sort((a: HistoryItem, b: HistoryItem) => b.ts - a.ts),
taskTitlesEnabled: taskTitlesEnabled ?? false,
soundEnabled: soundEnabled ?? false,
ttsEnabled: ttsEnabled ?? false,
ttsSpeed: ttsSpeed ?? 1.0,
Expand Down Expand Up @@ -2142,6 +2144,7 @@ export class ClineProvider
maxConcurrentFileReads: stateValues.maxConcurrentFileReads ?? 5,
historyPreviewCollapsed: stateValues.historyPreviewCollapsed ?? false,
reasoningBlockCollapsed: stateValues.reasoningBlockCollapsed ?? true,
taskTitlesEnabled: stateValues.taskTitlesEnabled ?? false,
cloudUserInfo,
cloudIsAuthenticated,
sharingEnabled,
Expand Down Expand Up @@ -2203,7 +2206,17 @@ export class ClineProvider
const existingItemIndex = history.findIndex((h) => h.id === item.id)

if (existingItemIndex !== -1) {
history[existingItemIndex] = item
const existingItem = history[existingItemIndex]
const hasTitleProp = Object.prototype.hasOwnProperty.call(item, "title")
const mergedItem: HistoryItem = {
...existingItem,
...item,
}
if (!hasTitleProp) {
mergedItem.title = existingItem.title
}

history[existingItemIndex] = mergedItem
} else {
history.push(item)
}
Expand Down
59 changes: 59 additions & 0 deletions src/core/webview/__tests__/ClineProvider.sticky-mode.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1219,4 +1219,63 @@ describe("ClineProvider - Sticky Mode", () => {
})
})
})

describe("updateTaskHistory", () => {
beforeEach(async () => {
await provider.resolveWebviewView(mockWebviewView)
})

it("preserves existing title when update omits the title property", async () => {
const baseItem: HistoryItem = {
id: "task-with-title",
number: 1,
ts: Date.now(),
task: "Original task",
tokensIn: 10,
tokensOut: 20,
cacheWrites: 0,
cacheReads: 0,
totalCost: 0,
title: "Custom title",
}

await provider.updateTaskHistory(baseItem)

const itemWithoutTitle = { ...baseItem }
delete (itemWithoutTitle as any).title
itemWithoutTitle.tokensIn = 42

await provider.updateTaskHistory(itemWithoutTitle as HistoryItem)

const history = mockContext.globalState.get("taskHistory") as HistoryItem[]
expect(history[0]?.title).toBe("Custom title")
})

it("allows clearing a title when explicitly set to undefined", async () => {
const baseItem: HistoryItem = {
id: "task-clear-title",
number: 1,
ts: Date.now(),
task: "Another task",
tokensIn: 5,
tokensOut: 15,
cacheWrites: 0,
cacheReads: 0,
totalCost: 0,
title: "Temporary title",
}

await provider.updateTaskHistory(baseItem)

const clearedItem: HistoryItem = {
...baseItem,
title: undefined,
}

await provider.updateTaskHistory(clearedItem)

const history = mockContext.globalState.get("taskHistory") as HistoryItem[]
expect(history[0]?.title).toBeUndefined()
})
})
})
56 changes: 56 additions & 0 deletions src/core/webview/webviewMessageHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
type Language,
type GlobalState,
type ClineMessage,
type HistoryItem,
type TelemetrySetting,
TelemetryEventName,
UserSettingsConfig,
Expand Down Expand Up @@ -673,6 +674,57 @@ export const webviewMessageHandler = async (
vscode.window.showErrorMessage(t("common:errors.share_task_failed"))
}
break
case "setTaskTitle": {
const ids = Array.isArray(message.ids)
? Array.from(
new Set(
message.ids.filter((id): id is string => typeof id === "string" && id.trim().length > 0),
),
)
: []
if (ids.length === 0) {
break
}

const rawTitle = message.text ?? ""
const trimmedTitle = rawTitle.trim()
const normalizedTitle = trimmedTitle.length > 0 ? trimmedTitle : undefined
const { taskHistory } = await provider.getState()
if (!Array.isArray(taskHistory) || taskHistory.length === 0) {
break
}

let hasUpdates = false
const historyById = new Map(taskHistory.map((item) => [item.id, item] as const))

for (const id of ids) {
const existingItem = historyById.get(id)
if (!existingItem) {
console.warn(`[setTaskTitle] Unable to locate task history item with id ${id}`)
continue
}

const normalizedExistingTitle =
existingItem.title && existingItem.title.trim().length > 0 ? existingItem.title.trim() : undefined
if (normalizedExistingTitle === normalizedTitle) {
continue
}

const updatedItem: HistoryItem = {
...existingItem,
title: normalizedTitle,
}

await provider.updateTaskHistory(updatedItem)
hasUpdates = true
}

if (hasUpdates) {
await provider.postStateToWebview()
}

break
}
case "showTaskWithId":
provider.showTaskWithId(message.text!)
break
Expand Down Expand Up @@ -1621,6 +1673,10 @@ export const webviewMessageHandler = async (
await updateGlobalState("reasoningBlockCollapsed", message.bool ?? true)
// No need to call postStateToWebview here as the UI already updated optimistically
break
case "setTaskTitlesEnabled":
await updateGlobalState("taskTitlesEnabled", message.bool ?? false)
await provider.postStateToWebview()
break
case "toggleApiConfigPin":
if (message.text) {
const currentPinned = getGlobalState("pinnedApiConfigs") ?? {}
Expand Down
2 changes: 2 additions & 0 deletions src/shared/ExtensionMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,7 @@ export type ExtensionState = Pick<
| "openRouterImageGenerationSelectedModel"
| "includeTaskHistoryInEnhance"
| "reasoningBlockCollapsed"
| "taskTitlesEnabled"
> & {
version: string
clineMessages: ClineMessage[]
Expand Down Expand Up @@ -327,6 +328,7 @@ export type ExtensionState = Pick<
renderContext: "sidebar" | "editor"
settingsImportedAt?: number
historyPreviewCollapsed?: boolean
taskTitlesEnabled?: boolean

cloudUserInfo: CloudUserInfo | null
cloudIsAuthenticated: boolean
Expand Down
2 changes: 2 additions & 0 deletions src/shared/WebviewMessage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export interface WebviewMessage {
| "importSettings"
| "exportSettings"
| "resetState"
| "setTaskTitle"
| "flushRouterModels"
| "requestRouterModels"
| "requestOpenAiModels"
Expand Down Expand Up @@ -195,6 +196,7 @@ export interface WebviewMessage {
| "profileThresholds"
| "setHistoryPreviewCollapsed"
| "setReasoningBlockCollapsed"
| "setTaskTitlesEnabled"
| "openExternal"
| "filterMarketplaceItems"
| "marketplaceButtonClicked"
Expand Down
Loading