Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
Loading