mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
feat: Add editable titles to tasks and task history
- Add optional title field to HistoryItem type schema - Add updateTaskTitle message types to Extension and Webview messages - Implement title update handler in webviewMessageHandler with trimming - Add title editing UI to TaskHeader component with inline edit mode - Update TaskItem component to display titles prominently in history - Add translations for title editing UI elements - Add tests for backend title update functionality Implements #8366
This commit is contained in:
parent
ded23b72b7
commit
da79983d45
10 changed files with 553 additions and 7 deletions
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
@ -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()
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}}",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@ export interface WebviewMessage {
|
|||
| "showTaskWithId"
|
||||
| "deleteTaskWithId"
|
||||
| "exportTaskWithId"
|
||||
| "updateTaskTitle"
|
||||
| "importSettings"
|
||||
| "exportSettings"
|
||||
| "resetState"
|
||||
|
|
@ -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[]
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ import { useTranslation } from "react-i18next"
|
|||
import { useCloudUpsell } from "@src/hooks/useCloudUpsell"
|
||||
import { CloudUpsellDialog } from "@src/components/cloud/CloudUpsellDialog"
|
||||
import DismissibleUpsell from "@src/components/common/DismissibleUpsell"
|
||||
import { FoldVertical, ChevronUp, ChevronDown } from "lucide-react"
|
||||
import { FoldVertical, ChevronUp, ChevronDown, Edit2, Check, X } from "lucide-react"
|
||||
import prettyBytes from "pretty-bytes"
|
||||
|
||||
import type { ClineMessage } from "@roo-code/types"
|
||||
|
|
@ -16,6 +16,7 @@ import { cn } from "@src/lib/utils"
|
|||
import { StandardTooltip } from "@src/components/ui"
|
||||
import { useExtensionState } from "@src/context/ExtensionStateContext"
|
||||
import { useSelectedModel } from "@/components/ui/hooks/useSelectedModel"
|
||||
import { vscode } from "@src/utils/vscode"
|
||||
|
||||
import Thumbnails from "../common/Thumbnails"
|
||||
|
||||
|
|
@ -54,6 +55,9 @@ const TaskHeader = ({
|
|||
const { id: modelId, info: model } = useSelectedModel(apiConfiguration)
|
||||
const [isTaskExpanded, setIsTaskExpanded] = useState(false)
|
||||
const [showLongRunningTaskMessage, setShowLongRunningTaskMessage] = useState(false)
|
||||
const [isEditingTitle, setIsEditingTitle] = useState(false)
|
||||
const [editedTitle, setEditedTitle] = useState(currentTaskItem?.title || "")
|
||||
const titleInputRef = useRef<HTMLInputElement>(null)
|
||||
const { isOpen, openUpsell, closeUpsell, handleConnect } = useCloudUpsell({
|
||||
autoOpenOnAuth: false,
|
||||
})
|
||||
|
|
@ -82,6 +86,36 @@ const TaskHeader = ({
|
|||
return () => clearTimeout(timer)
|
||||
}, [currentTaskItem, isTaskComplete])
|
||||
|
||||
// Update edited title when currentTaskItem changes
|
||||
useEffect(() => {
|
||||
setEditedTitle(currentTaskItem?.title || "")
|
||||
}, [currentTaskItem?.title])
|
||||
|
||||
// Focus input when editing starts
|
||||
useEffect(() => {
|
||||
if (isEditingTitle && titleInputRef.current) {
|
||||
titleInputRef.current.focus()
|
||||
titleInputRef.current.select()
|
||||
}
|
||||
}, [isEditingTitle])
|
||||
|
||||
const handleSaveTitle = () => {
|
||||
if (currentTaskItem) {
|
||||
// Send message to update the task title
|
||||
vscode.postMessage({
|
||||
type: "updateTaskTitle",
|
||||
taskId: currentTaskItem.id,
|
||||
title: editedTitle.trim(),
|
||||
})
|
||||
}
|
||||
setIsEditingTitle(false)
|
||||
}
|
||||
|
||||
const handleCancelEdit = () => {
|
||||
setEditedTitle(currentTaskItem?.title || "")
|
||||
setIsEditingTitle(false)
|
||||
}
|
||||
|
||||
const textContainerRef = useRef<HTMLDivElement>(null)
|
||||
const textRef = useRef<HTMLDivElement>(null)
|
||||
const contextWindow = model?.contextWindow || 1
|
||||
|
|
@ -143,11 +177,65 @@ const TaskHeader = ({
|
|||
<div className="flex justify-between items-center gap-0">
|
||||
<div className="flex items-center select-none grow min-w-0">
|
||||
<div className="whitespace-nowrap overflow-hidden text-ellipsis grow min-w-0">
|
||||
{isTaskExpanded && <span className="font-bold">{t("chat:task.title")}</span>}
|
||||
{isTaskExpanded && (
|
||||
<div className="flex items-center gap-2">
|
||||
{isEditingTitle ? (
|
||||
<div className="flex items-center gap-1" onClick={(e) => e.stopPropagation()}>
|
||||
<input
|
||||
ref={titleInputRef}
|
||||
type="text"
|
||||
value={editedTitle}
|
||||
onChange={(e) => setEditedTitle(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
handleSaveTitle()
|
||||
} else if (e.key === "Escape") {
|
||||
handleCancelEdit()
|
||||
}
|
||||
}}
|
||||
className="px-2 py-0.5 bg-vscode-input-background border border-vscode-input-border rounded text-vscode-input-foreground"
|
||||
placeholder={t("chat:task.titlePlaceholder")}
|
||||
/>
|
||||
<StandardTooltip content={t("chat:task.saveTitle")}>
|
||||
<button
|
||||
onClick={handleSaveTitle}
|
||||
className="p-1 hover:bg-vscode-button-hoverBackground rounded">
|
||||
<Check size={14} />
|
||||
</button>
|
||||
</StandardTooltip>
|
||||
<StandardTooltip content={t("chat:task.cancelEdit")}>
|
||||
<button
|
||||
onClick={handleCancelEdit}
|
||||
className="p-1 hover:bg-vscode-button-hoverBackground rounded">
|
||||
<X size={14} />
|
||||
</button>
|
||||
</StandardTooltip>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<span className="font-bold">
|
||||
{currentTaskItem?.title || t("chat:task.title")}
|
||||
</span>
|
||||
<StandardTooltip content={t("chat:task.editTitle")}>
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
setIsEditingTitle(true)
|
||||
}}
|
||||
className="p-1 opacity-60 hover:opacity-100 hover:bg-vscode-button-hoverBackground rounded">
|
||||
<Edit2 size={14} />
|
||||
</button>
|
||||
</StandardTooltip>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{!isTaskExpanded && (
|
||||
<div>
|
||||
<span className="font-bold mr-1">{t("chat:task.title")}</span>
|
||||
<Mention text={task.text} />
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="font-bold mr-1">
|
||||
{currentTaskItem?.title || t("chat:task.title")}
|
||||
</span>
|
||||
{!currentTaskItem?.title && <Mention text={task.text} />}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ vi.mock("@vscode/webview-ui-toolkit/react", () => ({
|
|||
// Create a variable to hold the mock state
|
||||
let mockExtensionState: {
|
||||
apiConfiguration: ProviderSettings
|
||||
currentTaskItem: { id: string } | null
|
||||
currentTaskItem: { id: string; title?: string } | null
|
||||
clineMessages: any[]
|
||||
} = {
|
||||
apiConfiguration: {
|
||||
|
|
@ -357,4 +357,223 @@ describe("TaskHeader", () => {
|
|||
expect(screen.getByTestId("dismissible-upsell")).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
describe("Title editing functionality", () => {
|
||||
beforeEach(() => {
|
||||
// Reset the mock state before each test
|
||||
mockExtensionState = {
|
||||
apiConfiguration: {
|
||||
apiProvider: "anthropic",
|
||||
apiKey: "test-api-key",
|
||||
apiModelId: "claude-3-opus-20240229",
|
||||
} as ProviderSettings,
|
||||
currentTaskItem: { id: "test-task-id", title: "Existing Title" },
|
||||
clineMessages: [],
|
||||
}
|
||||
// Clear mock calls
|
||||
vi.clearAllMocks()
|
||||
})
|
||||
|
||||
it("should display the title if it exists", () => {
|
||||
renderTaskHeader()
|
||||
expect(screen.getByText("Existing Title")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should display task text when no title exists", () => {
|
||||
mockExtensionState.currentTaskItem = { id: "test-task-id" }
|
||||
renderTaskHeader()
|
||||
expect(screen.getByText("Test task")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should show edit button when hovering over title", () => {
|
||||
renderTaskHeader()
|
||||
const titleElement = screen.getByText("Existing Title")
|
||||
|
||||
// Initially, edit button should not be visible
|
||||
expect(screen.queryByLabelText("chat:editTitle")).not.toBeInTheDocument()
|
||||
|
||||
// Hover over the title
|
||||
fireEvent.mouseEnter(titleElement.parentElement!)
|
||||
|
||||
// Edit button should now be visible
|
||||
expect(screen.getByLabelText("chat:editTitle")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should enter edit mode when edit button is clicked", () => {
|
||||
renderTaskHeader()
|
||||
const titleElement = screen.getByText("Existing Title")
|
||||
|
||||
// Hover and click edit button
|
||||
fireEvent.mouseEnter(titleElement.parentElement!)
|
||||
const editButton = screen.getByLabelText("chat:editTitle")
|
||||
fireEvent.click(editButton)
|
||||
|
||||
// Should show input field with current title
|
||||
const input = screen.getByDisplayValue("Existing Title") as HTMLInputElement
|
||||
expect(input).toBeInTheDocument()
|
||||
expect(input.tagName).toBe("INPUT")
|
||||
|
||||
// Should show save and cancel buttons
|
||||
expect(screen.getByLabelText("chat:saveTitle")).toBeInTheDocument()
|
||||
expect(screen.getByLabelText("chat:cancelEdit")).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should save title when save button is clicked", () => {
|
||||
const { vscode } = require("@/utils/vscode")
|
||||
renderTaskHeader()
|
||||
|
||||
// Enter edit mode
|
||||
const titleElement = screen.getByText("Existing Title")
|
||||
fireEvent.mouseEnter(titleElement.parentElement!)
|
||||
fireEvent.click(screen.getByLabelText("chat:editTitle"))
|
||||
|
||||
// Change the title
|
||||
const input = screen.getByDisplayValue("Existing Title") as HTMLInputElement
|
||||
fireEvent.change(input, { target: { value: "New Title" } })
|
||||
|
||||
// Click save
|
||||
fireEvent.click(screen.getByLabelText("chat:saveTitle"))
|
||||
|
||||
// Should post message to update title
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "updateTaskTitle",
|
||||
taskId: "test-task-id",
|
||||
title: "New Title",
|
||||
})
|
||||
})
|
||||
|
||||
it("should cancel editing when cancel button is clicked", () => {
|
||||
renderTaskHeader()
|
||||
|
||||
// Enter edit mode
|
||||
const titleElement = screen.getByText("Existing Title")
|
||||
fireEvent.mouseEnter(titleElement.parentElement!)
|
||||
fireEvent.click(screen.getByLabelText("chat:editTitle"))
|
||||
|
||||
// Change the title
|
||||
const input = screen.getByDisplayValue("Existing Title") as HTMLInputElement
|
||||
fireEvent.change(input, { target: { value: "Changed Title" } })
|
||||
|
||||
// Click cancel
|
||||
fireEvent.click(screen.getByLabelText("chat:cancelEdit"))
|
||||
|
||||
// Should exit edit mode and show original title
|
||||
expect(screen.getByText("Existing Title")).toBeInTheDocument()
|
||||
expect(screen.queryByDisplayValue("Changed Title")).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should save title when Enter key is pressed", () => {
|
||||
const { vscode } = require("@/utils/vscode")
|
||||
renderTaskHeader()
|
||||
|
||||
// Enter edit mode
|
||||
const titleElement = screen.getByText("Existing Title")
|
||||
fireEvent.mouseEnter(titleElement.parentElement!)
|
||||
fireEvent.click(screen.getByLabelText("chat:editTitle"))
|
||||
|
||||
// Change the title and press Enter
|
||||
const input = screen.getByDisplayValue("Existing Title") as HTMLInputElement
|
||||
fireEvent.change(input, { target: { value: "Enter Key Title" } })
|
||||
fireEvent.keyDown(input, { key: "Enter" })
|
||||
|
||||
// Should post message to update title
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "updateTaskTitle",
|
||||
taskId: "test-task-id",
|
||||
title: "Enter Key Title",
|
||||
})
|
||||
})
|
||||
|
||||
it("should cancel editing when Escape key is pressed", () => {
|
||||
renderTaskHeader()
|
||||
|
||||
// Enter edit mode
|
||||
const titleElement = screen.getByText("Existing Title")
|
||||
fireEvent.mouseEnter(titleElement.parentElement!)
|
||||
fireEvent.click(screen.getByLabelText("chat:editTitle"))
|
||||
|
||||
// Change the title and press Escape
|
||||
const input = screen.getByDisplayValue("Existing Title") as HTMLInputElement
|
||||
fireEvent.change(input, { target: { value: "Escape Test" } })
|
||||
fireEvent.keyDown(input, { key: "Escape" })
|
||||
|
||||
// Should exit edit mode and show original title
|
||||
expect(screen.getByText("Existing Title")).toBeInTheDocument()
|
||||
expect(screen.queryByDisplayValue("Escape Test")).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should clear title when empty string is saved", () => {
|
||||
const { vscode } = require("@/utils/vscode")
|
||||
renderTaskHeader()
|
||||
|
||||
// Enter edit mode
|
||||
const titleElement = screen.getByText("Existing Title")
|
||||
fireEvent.mouseEnter(titleElement.parentElement!)
|
||||
fireEvent.click(screen.getByLabelText("chat:editTitle"))
|
||||
|
||||
// Clear the title
|
||||
const input = screen.getByDisplayValue("Existing Title") as HTMLInputElement
|
||||
fireEvent.change(input, { target: { value: "" } })
|
||||
|
||||
// Click save
|
||||
fireEvent.click(screen.getByLabelText("chat:saveTitle"))
|
||||
|
||||
// Should post message with empty title
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "updateTaskTitle",
|
||||
taskId: "test-task-id",
|
||||
title: "",
|
||||
})
|
||||
})
|
||||
|
||||
it("should show placeholder when no title exists and in edit mode", () => {
|
||||
mockExtensionState.currentTaskItem = { id: "test-task-id" }
|
||||
renderTaskHeader()
|
||||
|
||||
// Enter edit mode
|
||||
const taskElement = screen.getByText("Test task")
|
||||
fireEvent.mouseEnter(taskElement.parentElement!)
|
||||
fireEvent.click(screen.getByLabelText("chat:editTitle"))
|
||||
|
||||
// Should show input with placeholder
|
||||
const input = screen.getByPlaceholderText("chat:titlePlaceholder") as HTMLInputElement
|
||||
expect(input).toBeInTheDocument()
|
||||
expect(input.value).toBe("")
|
||||
})
|
||||
|
||||
it("should not show edit button when task is null", () => {
|
||||
mockExtensionState.currentTaskItem = null
|
||||
renderTaskHeader()
|
||||
|
||||
const taskElement = screen.getByText("Test task")
|
||||
fireEvent.mouseEnter(taskElement.parentElement!)
|
||||
|
||||
// Edit button should not be present
|
||||
expect(screen.queryByLabelText("chat:editTitle")).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it("should trim whitespace from saved title", () => {
|
||||
const { vscode } = require("@/utils/vscode")
|
||||
renderTaskHeader()
|
||||
|
||||
// Enter edit mode
|
||||
const titleElement = screen.getByText("Existing Title")
|
||||
fireEvent.mouseEnter(titleElement.parentElement!)
|
||||
fireEvent.click(screen.getByLabelText("chat:editTitle"))
|
||||
|
||||
// Add title with whitespace
|
||||
const input = screen.getByDisplayValue("Existing Title") as HTMLInputElement
|
||||
fireEvent.change(input, { target: { value: " Trimmed Title " } })
|
||||
|
||||
// Click save
|
||||
fireEvent.click(screen.getByLabelText("chat:saveTitle"))
|
||||
|
||||
// Should post message with trimmed title
|
||||
expect(vscode.postMessage).toHaveBeenCalledWith({
|
||||
type: "updateTaskTitle",
|
||||
taskId: "test-task-id",
|
||||
title: "Trimmed Title",
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -68,11 +68,22 @@ const TaskItem = ({
|
|||
)}
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
{/* Display title if it exists */}
|
||||
{item.title && (
|
||||
<div
|
||||
className={cn("font-semibold text-vscode-foreground mb-1", {
|
||||
"text-base": !isCompact,
|
||||
})}>
|
||||
{item.title}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"overflow-hidden whitespace-pre-wrap text-vscode-foreground text-ellipsis line-clamp-2",
|
||||
"overflow-hidden whitespace-pre-wrap text-ellipsis line-clamp-2",
|
||||
{
|
||||
"text-base": !isCompact,
|
||||
"text-vscode-descriptionForeground": item.title, // Make task text secondary if title exists
|
||||
"text-vscode-foreground": !item.title, // Primary color if no title
|
||||
},
|
||||
!isCompact && isSelectionMode ? "mb-1" : "",
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,10 @@
|
|||
"greeting": "Welcome to Roo Code!",
|
||||
"task": {
|
||||
"title": "Task",
|
||||
"titlePlaceholder": "Enter a title for this task...",
|
||||
"editTitle": "Edit task title",
|
||||
"saveTitle": "Save title",
|
||||
"cancelEdit": "Cancel editing",
|
||||
"expand": "Expand task",
|
||||
"collapse": "Collapse task",
|
||||
"seeMore": "See more",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue