ux: Improvements to to-do lists and task headers (#9096)

Co-authored-by: roomote[bot] <219738659+roomote[bot]@users.noreply.github.com>
Co-authored-by: Matt Rubens <mrubens@users.noreply.github.com>
This commit is contained in:
Bruno Bergher 2025-11-07 22:26:20 +00:00 committed by GitHub
parent fc2147ab4f
commit fff5cf804f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 537 additions and 448 deletions

View file

@ -18,6 +18,7 @@ import { formatPathTooltip } from "@src/utils/formatPathTooltip"
import { ToolUseBlock, ToolUseBlockHeader } from "../common/ToolUseBlock"
import UpdateTodoListToolBlock from "./UpdateTodoListToolBlock"
import { TodoChangeDisplay } from "./TodoChangeDisplay"
import CodeAccordian from "../common/CodeAccordian"
import MarkdownBlock from "../common/MarkdownBlock"
import { ReasoningBlock } from "./ReasoningBlock"
@ -62,6 +63,39 @@ import {
import { cn } from "@/lib/utils"
import { PathTooltip } from "../ui/PathTooltip"
// Helper function to get previous todos before a specific message
function getPreviousTodos(messages: ClineMessage[], currentMessageTs: number): any[] {
// Find the previous updateTodoList message before the current one
const previousUpdateIndex = messages
.slice()
.reverse()
.findIndex((msg) => {
if (msg.ts >= currentMessageTs) return false
if (msg.type === "ask" && msg.ask === "tool") {
try {
const tool = JSON.parse(msg.text || "{}")
return tool.tool === "updateTodoList"
} catch {
return false
}
}
return false
})
if (previousUpdateIndex !== -1) {
const previousMessage = messages.slice().reverse()[previousUpdateIndex]
try {
const tool = JSON.parse(previousMessage.text || "{}")
return tool.todos || []
} catch {
return []
}
}
// If no previous updateTodoList message, return empty array
return []
}
interface ChatRowProps {
message: ClineMessage
lastModifiedMessage?: ClineMessage
@ -127,11 +161,10 @@ export const ChatRowContent = ({
onFollowUpUnmount,
onBatchFileResponse,
isFollowUpAnswered,
editable,
}: ChatRowContentProps) => {
const { t } = useTranslation()
const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode, apiConfiguration } = useExtensionState()
const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode, apiConfiguration, clineMessages } = useExtensionState()
const { info: model } = useSelectedModel(apiConfiguration)
const [isEditing, setIsEditing] = useState(false)
const [editedContent, setEditedContent] = useState("")
@ -503,18 +536,11 @@ export const ChatRowContent = ({
}
case "updateTodoList" as any: {
const todos = (tool as any).todos || []
return (
<UpdateTodoListToolBlock
todos={todos}
content={tool.content}
onChange={(updatedTodos) => {
if (typeof vscode !== "undefined" && vscode?.postMessage) {
vscode.postMessage({ type: "updateTodoList", payload: { todos: updatedTodos } })
}
}}
editable={editable && isLast}
/>
)
// Get previous todos from the latest todos in the task context
const previousTodos = getPreviousTodos(clineMessages, message.ts)
return <TodoChangeDisplay previousTodos={previousTodos} newTodos={todos} />
}
case "newFileCreated":
return (

View file

@ -1,14 +1,15 @@
import { useState, useEffect, useCallback } from "react"
import { useTranslation } from "react-i18next"
import { CloudUpload, Copy, Check } from "lucide-react"
import { Copy, Check, CloudUploadIcon } from "lucide-react"
import QRCode from "qrcode"
import type { HistoryItem } from "@roo-code/types"
import { useExtensionState } from "@/context/ExtensionStateContext"
import { useCopyToClipboard } from "@/utils/clipboard"
import { Button, Dialog, DialogContent, DialogHeader, DialogTitle, Input, StandardTooltip } from "@/components/ui"
import { Button, Dialog, DialogContent, DialogHeader, DialogTitle, Input } from "@/components/ui"
import { vscode } from "@/utils/vscode"
import { LucideIconButton } from "./LucideIconButton"
interface CloudTaskButtonProps {
item?: HistoryItem
@ -83,18 +84,12 @@ export const CloudTaskButton = ({ item, disabled = false }: CloudTaskButtonProps
return (
<>
<StandardTooltip content={t("chat:task.openInCloud")}>
<Button
variant="ghost"
size="icon"
disabled={disabled}
className="h-7 w-7 p-1.5 hover:bg-vscode-toolbar-hoverBackground"
onClick={() => setDialogOpen(true)}
data-testid="cloud-task-button"
aria-label={t("chat:task.openInCloud")}>
<CloudUpload className="h-4 w-4" />
</Button>
</StandardTooltip>
<LucideIconButton
icon={CloudUploadIcon}
disabled={disabled}
data-testid="cloud-task-button"
title={t("chat:task.openInCloud")}
onClick={() => setDialogOpen(true)}></LucideIconButton>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
<DialogContent className="max-w-100">

View file

@ -0,0 +1,50 @@
import { cn } from "@src/lib/utils"
import { Button, StandardTooltip } from "@src/components/ui"
import { Loader2, LucideIcon } from "lucide-react"
interface LucideIconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
icon: LucideIcon
title: string
disabled?: boolean
tooltip?: boolean
isLoading?: boolean
style?: React.CSSProperties
}
export const LucideIconButton: React.FC<LucideIconButtonProps> = ({
icon,
title,
className,
disabled,
tooltip = true,
isLoading,
onClick,
style,
...props
}) => {
const Icon = icon
return (
<StandardTooltip content={tooltip ? title : undefined}>
<Button
aria-label={title}
className={cn(
"relative inline-flex items-center justify-center",
"bg-transparent border-none p-1.5",
"rounded-full",
"text-vscode-foreground opacity-85",
"transition-all duration-150",
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
"active:bg-[rgba(255,255,255,0.1)]",
!disabled && "cursor-pointer hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)]",
disabled && "cursor-not-allowed opacity-40 hover:bg-transparent active:bg-transparent",
className,
)}
disabled={disabled}
onClick={!disabled ? onClick : undefined}
style={{ fontSize: 16.5, ...style }}
{...props}>
{isLoading ? <Loader2 className="size-2.5 animate-spin" /> : <Icon className="size-2.5" />}
</Button>
</StandardTooltip>
)
}

View file

@ -21,7 +21,7 @@ export const Mention = ({ text, withShadow = false }: MentionProps) => {
return (
<span
key={index}
className={`${withShadow ? "mention-context-highlight-with-shadow" : "mention-context-highlight"} cursor-pointer`}
className={`${withShadow ? "mention-context-highlight-with-shadow" : "mention-context-highlight"} text-[0.9em] cursor-pointer`}
onClick={() => vscode.postMessage({ type: "openMention", text: part })}>
@{part}
</span>

View file

@ -1,6 +1,6 @@
import { useState, useEffect } from "react"
import { useTranslation } from "react-i18next"
import { Share2 } from "lucide-react"
import { Share2Icon } from "lucide-react"
import { type HistoryItem, type ShareVisibility, TelemetryEventName } from "@roo-code/types"
@ -10,7 +10,6 @@ import { useExtensionState } from "@/context/ExtensionStateContext"
import { useCloudUpsell } from "@/hooks/useCloudUpsell"
import { CloudUpsellDialog } from "@/components/cloud/CloudUpsellDialog"
import {
Button,
Popover,
PopoverContent,
PopoverTrigger,
@ -20,14 +19,14 @@ import {
CommandGroup,
StandardTooltip,
} from "@/components/ui"
import { LucideIconButton } from "./LucideIconButton"
interface ShareButtonProps {
item?: HistoryItem
disabled?: boolean
showLabel?: boolean
}
export const ShareButton = ({ item, disabled = false, showLabel = false }: ShareButtonProps) => {
export const ShareButton = ({ item, disabled = false }: ShareButtonProps) => {
const [shareDropdownOpen, setShareDropdownOpen] = useState(false)
const [shareSuccess, setShareSuccess] = useState<{ visibility: ShareVisibility; url: string } | null>(null)
const [wasConnectInitiatedFromShare, setWasConnectInitiatedFromShare] = useState(false)
@ -153,20 +152,13 @@ export const ShareButton = ({ item, disabled = false, showLabel = false }: Share
<Popover open={shareDropdownOpen} onOpenChange={setShareDropdownOpen}>
<StandardTooltip content={shareButtonState.title}>
<PopoverTrigger asChild>
<Button
variant="ghost"
size={showLabel ? "sm" : "icon"}
<LucideIconButton
icon={Share2Icon}
disabled={disabled || shareButtonState.disabled}
className={
showLabel
? "h-7 px-2 hover:bg-vscode-toolbar-hoverBackground"
: "h-7 w-7 p-1.5 hover:bg-vscode-toolbar-hoverBackground"
}
tooltip={false}
onClick={handleShareButtonClick}
data-testid="share-button">
<Share2 />
{showLabel && <span className="ml-0">{t("chat:task.share")}</span>}
</Button>
data-testid="share-button"
title={t("chat:task.share")}></LucideIconButton>
</PopoverTrigger>
</StandardTooltip>
@ -221,22 +213,12 @@ export const ShareButton = ({ item, disabled = false, showLabel = false }: Share
</PopoverContent>
</Popover>
) : (
<StandardTooltip content={shareButtonState.title}>
<Button
variant="ghost"
size={showLabel ? "sm" : "icon"}
disabled={disabled || shareButtonState.disabled}
className={
showLabel
? "h-7 px-2 hover:bg-vscode-toolbar-hoverBackground"
: "h-7 w-7 p-1.5 hover:bg-vscode-toolbar-hoverBackground"
}
onClick={handleShareButtonClick}
data-testid="share-button">
<Share2 />
{showLabel && <span className="ml-1">{t("chat:task.share")}</span>}
</Button>
</StandardTooltip>
<LucideIconButton
icon={Share2Icon}
disabled={disabled || shareButtonState.disabled}
title={shareButtonState.title}
onClick={handleShareButtonClick}
data-testid="share-button"></LucideIconButton>
)}
{/* Connect to Cloud Modal */}

View file

@ -7,9 +7,10 @@ import { vscode } from "@/utils/vscode"
import { useCopyToClipboard } from "@/utils/clipboard"
import { DeleteTaskDialog } from "../history/DeleteTaskDialog"
import { IconButton } from "./IconButton"
import { ShareButton } from "./ShareButton"
import { CloudTaskButton } from "./CloudTaskButton"
import { CopyIcon, DownloadIcon, Trash2Icon } from "lucide-react"
import { LucideIconButton } from "./LucideIconButton"
interface TaskActionsProps {
item?: HistoryItem
@ -19,40 +20,38 @@ interface TaskActionsProps {
export const TaskActions = ({ item, buttonsDisabled }: TaskActionsProps) => {
const [deleteTaskId, setDeleteTaskId] = useState<string | null>(null)
const { t } = useTranslation()
const { copyWithFeedback, showCopyFeedback } = useCopyToClipboard()
const { copyWithFeedback } = useCopyToClipboard()
return (
<div className="flex flex-row items-center">
<IconButton
iconClass="codicon-desktop-download"
<div className="flex flex-row items-center -ml-0.5 mt-1 gap-1">
<LucideIconButton
icon={DownloadIcon}
title={t("chat:task.export")}
onClick={() => vscode.postMessage({ type: "exportCurrentTask" })}
/>
{item?.task && (
<IconButton
iconClass={showCopyFeedback ? "codicon-check" : "codicon-copy"}
<LucideIconButton
icon={CopyIcon}
title={t("history:copyPrompt")}
onClick={(e) => copyWithFeedback(item.task, e)}
/>
)}
{!!item?.size && item.size > 0 && (
<>
<div className="flex items-center">
<IconButton
iconClass="codicon-trash"
title={t("chat:task.delete")}
disabled={buttonsDisabled}
onClick={(e) => {
e.stopPropagation()
if (e.shiftKey) {
vscode.postMessage({ type: "deleteTaskWithId", text: item.id })
} else {
setDeleteTaskId(item.id)
}
}}
/>
</div>
<LucideIconButton
icon={Trash2Icon}
title={t("chat:task.delete")}
disabled={buttonsDisabled}
onClick={(e) => {
e.stopPropagation()
if (e.shiftKey) {
vscode.postMessage({ type: "deleteTaskWithId", text: item.id })
} else {
setDeleteTaskId(item.id)
}
}}
/>
{deleteTaskId && (
<DeleteTaskDialog
taskId={deleteTaskId}
@ -62,7 +61,7 @@ export const TaskActions = ({ item, buttonsDisabled }: TaskActionsProps) => {
)}
</>
)}
<ShareButton item={item} disabled={false} showLabel={false} />
<ShareButton item={item} disabled={false} />
<CloudTaskButton item={item} disabled={buttonsDisabled} />
</div>
)

View file

@ -3,7 +3,15 @@ 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 {
ChevronUp,
ChevronDown,
SquarePen,
Coins,
HardDriveDownload,
HardDriveUpload,
FoldVerticalIcon,
} from "lucide-react"
import prettyBytes from "pretty-bytes"
import type { ClineMessage } from "@roo-code/types"
@ -23,6 +31,7 @@ import { TaskActions } from "./TaskActions"
import { ContextWindowProgress } from "./ContextWindowProgress"
import { Mention } from "./Mention"
import { TodoListDisplay } from "./TodoListDisplay"
import { LucideIconButton } from "./LucideIconButton"
export interface TaskHeaderProps {
task: ClineMessage
@ -87,20 +96,18 @@ const TaskHeader = ({
const contextWindow = model?.contextWindow || 1
const condenseButton = (
<StandardTooltip content={t("chat:task.condenseContext")}>
<button
disabled={buttonsDisabled}
onClick={() => currentTaskItem && handleCondenseContext(currentTaskItem.id)}
className="shrink-0 min-h-[20px] min-w-[20px] p-[2px] cursor-pointer disabled:cursor-not-allowed opacity-85 hover:opacity-100 bg-transparent border-none rounded-md">
<FoldVertical size={16} />
</button>
</StandardTooltip>
<LucideIconButton
title={t("chat:task.condenseContext")}
icon={FoldVerticalIcon}
disabled={buttonsDisabled}
onClick={() => currentTaskItem && handleCondenseContext(currentTaskItem.id)}
/>
)
const hasTodos = todos && Array.isArray(todos) && todos.length > 0
return (
<div className="pt-2 pb-0 px-3">
<div className="group pt-2 pb-0 px-3">
{showLongRunningTaskMessage && !isTaskComplete && (
<DismissibleUpsell
upsellId="longRunningTask"
@ -115,10 +122,15 @@ const TaskHeader = ({
"px-3 pt-2.5 pb-2 flex flex-col gap-1.5 relative z-1 cursor-pointer",
"bg-vscode-input-background hover:bg-vscode-input-background/90",
"text-vscode-foreground/80 hover:text-vscode-foreground",
"shadow-sm shadow-black/30 rounded-xl",
"shadow-lg shadow-vscode-sideBar-background/50 rounded-xl",
hasTodos && "border-b-0",
)}
onClick={(e) => {
// Don't expand if clicking on todos section
if (e.target instanceof Element && e.target.closest("[data-todo-list]")) {
return
}
// Don't expand if clicking on buttons or interactive elements
if (
e.target instanceof Element &&
@ -142,12 +154,14 @@ 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">
<div className="grow min-w-0">
{isTaskExpanded && <span className="font-bold">{t("chat:task.title")}</span>}
{!isTaskExpanded && (
<div>
<span className="font-bold mr-1">{t("chat:task.title")}</span>
<Mention text={task.text} />
<div className="flex items-center gap-2">
<SquarePen className="size-3 shrink-0" />
<span className="whitespace-nowrap overflow-hidden text-ellipsis">
<Mention text={task.text} />
</span>
</div>
)}
</div>
@ -156,7 +170,11 @@ const TaskHeader = ({
<button
onClick={() => setIsTaskExpanded(!isTaskExpanded)}
className="shrink-0 min-h-[20px] min-w-[20px] p-[2px] cursor-pointer opacity-85 hover:opacity-100 bg-transparent border-none rounded-md">
{isTaskExpanded ? <ChevronUp size={16} /> : <ChevronDown size={16} />}
{isTaskExpanded ? (
<ChevronUp size={16} />
) : (
<ChevronDown size={16} className="opacity-0 group-hover:opacity-100" />
)}
</button>
</StandardTooltip>
</div>
@ -166,6 +184,7 @@ const TaskHeader = ({
<div
className="flex items-center gap-2 text-sm text-muted-foreground/70"
onClick={(e) => e.stopPropagation()}>
<Coins className="size-3 shrink-0" />
<StandardTooltip
content={
<div className="space-y-1">
@ -220,7 +239,7 @@ const TaskHeader = ({
className="text-vscode-font-size overflow-y-auto break-words break-anywhere relative">
<div
ref={textRef}
className="overflow-auto max-h-80 whitespace-pre-wrap break-words break-anywhere cursor-text"
className="overflow-auto max-h-80 whitespace-pre-wrap break-words break-anywhere cursor-text py-0.5"
style={{
display: "-webkit-box",
WebkitLineClamp: "unset",
@ -231,18 +250,22 @@ const TaskHeader = ({
</div>
{task.images && task.images.length > 0 && <Thumbnails images={task.images} />}
<div className="border-t border-b border-vscode-panel-border/50 py-4 mt-2 mb-1">
<table className="w-full">
<div onClick={(e) => e.stopPropagation()}>
<TaskActions item={currentTaskItem} buttonsDisabled={buttonsDisabled} />
</div>
<div className="pt-3 mt-2 -mx-2.5 px-2.5 border-t border-vscode-sideBar-background">
<table className="w-full text-sm">
<tbody>
{contextWindow > 0 && (
<tr>
<th
className="font-bold text-left align-top w-1 whitespace-nowrap pl-1 pr-3 h-[24px]"
className="font-medium text-left align-top w-1 whitespace-nowrap pr-3 h-[24px]"
data-testid="context-window-label">
{t("chat:task.contextWindow")}
</th>
<td className="align-top">
<div className={`max-w-80 -mt-0.5 flex flex-nowrap gap-1`}>
<td className="font-light align-top">
<div className={`max-w-md -mt-1.5 flex flex-nowrap gap-1`}>
<ContextWindowProgress
contextWindow={contextWindow}
contextTokens={contextTokens || 0}
@ -263,10 +286,10 @@ const TaskHeader = ({
)}
<tr>
<th className="font-bold text-left align-top w-1 whitespace-nowrap pl-1 pr-3 h-[24px]">
<th className="font-medium text-left align-top w-1 whitespace-nowrap pr-3 h-[24px]">
{t("chat:task.tokens")}
</th>
<td className="align-top">
<td className="font-light align-top">
<div className="flex items-center gap-1 flex-wrap">
{typeof tokensIn === "number" && tokensIn > 0 && (
<span> {formatLargeNumber(tokensIn)}</span>
@ -281,16 +304,22 @@ const TaskHeader = ({
{((typeof cacheReads === "number" && cacheReads > 0) ||
(typeof cacheWrites === "number" && cacheWrites > 0)) && (
<tr>
<th className="font-bold text-left align-top w-1 whitespace-nowrap pl-1 pr-3 h-[24px]">
<th className="font-medium text-left align-top w-1 whitespace-nowrap pr-3 h-[24px]">
{t("chat:task.cache")}
</th>
<td className="align-top">
<td className="font-light align-top">
<div className="flex items-center gap-1 flex-wrap">
{typeof cacheWrites === "number" && cacheWrites > 0 && (
<span> {formatLargeNumber(cacheWrites)}</span>
<>
<HardDriveDownload className="size-2.5" />
<span>{formatLargeNumber(cacheWrites)}</span>
</>
)}
{typeof cacheReads === "number" && cacheReads > 0 && (
<span> {formatLargeNumber(cacheReads)}</span>
<>
<HardDriveUpload className="size-2.5" />
<span>{formatLargeNumber(cacheReads)}</span>
</>
)}
</div>
</td>
@ -299,10 +328,10 @@ const TaskHeader = ({
{!!totalCost && (
<tr>
<th className="font-bold text-left align-top w-1 whitespace-nowrap pl-1 pr-3 h-[24px]">
<th className="font-medium text-left align-top w-1 whitespace-nowrap pr-3 h-[24px]">
{t("chat:task.apiCost")}
</th>
<td className="align-top">
<td className="font-light align-top">
<span>${totalCost?.toFixed(2)}</span>
</td>
</tr>
@ -311,24 +340,22 @@ const TaskHeader = ({
{/* Size display */}
{!!currentTaskItem?.size && currentTaskItem.size > 0 && (
<tr>
<th className="font-bold text-left align-top w-1 whitespace-nowrap pl-1 pr-2 h-[20px]">
<th className="font-medium text-left align-top w-1 whitespace-nowrap pr-2 h-[20px]">
{t("chat:task.size")}
</th>
<td className="align-top">{prettyBytes(currentTaskItem.size)}</td>
<td className="font-light align-top">
{prettyBytes(currentTaskItem.size)}
</td>
</tr>
)}
</tbody>
</table>
</div>
{/* Footer with task management buttons */}
<div onClick={(e) => e.stopPropagation()}>
<TaskActions item={currentTaskItem} buttonsDisabled={buttonsDisabled} />
</div>
</>
)}
{/* Todo list - always shown at bottom when todos exist */}
{hasTodos && <TodoListDisplay todos={todos ?? (task as any)?.tool?.todos ?? []} />}
</div>
<TodoListDisplay todos={todos ?? (task as any)?.tool?.todos ?? []} />
<CloudUpsellDialog open={isOpen} onOpenChange={closeUpsell} onConnect={handleConnect} />
</div>
)

View file

@ -0,0 +1,131 @@
import { t } from "i18next"
import { ArrowRight, Check, ListChecks, SquareDashed } from "lucide-react"
type TodoStatus = "completed" | "in_progress" | "pending"
interface TodoItem {
id?: string
content: string
status?: TodoStatus | string
}
interface TodoChangeDisplayProps {
previousTodos: TodoItem[]
newTodos: TodoItem[]
}
interface TodoGroup {
todos: TodoItem[]
status: TodoStatus | null
keyPrefix: string
className?: string
}
function getTodoIcon(status: TodoStatus | null) {
switch (status) {
case "completed":
return <Check className="size-3 mt-1 shrink-0" />
case "in_progress":
return <ArrowRight className="size-3 mt-1 shrink-0" />
default:
return <SquareDashed className="size-3 mt-1 shrink-0" />
}
}
function TodoList({ todos, status, keyPrefix, className }: TodoGroup) {
if (todos.length === 0) return null
return (
<ul className="list-none space-y-1 my-1">
{todos.map((todo) => {
const icon = getTodoIcon(status)
return (
<li
key={`${keyPrefix}-${todo.id || todo.content}`}
className={`flex flex-row gap-2 items-start ${className || ""}`}>
{icon}
<span>{todo.content}</span>
</li>
)
})}
</ul>
)
}
export function TodoChangeDisplay({ previousTodos, newTodos }: TodoChangeDisplayProps) {
const isInitialState = previousTodos.length === 0
// Determine which todos to display
let todoGroups: TodoGroup[]
if (isInitialState && newTodos.length > 0) {
// For initial state, show all todos grouped by status
todoGroups = [
{
todos: newTodos.filter((todo) => !todo.status || todo.status === "pending"),
status: null,
keyPrefix: "pending",
},
{
todos: newTodos.filter((todo) => todo.status === "in_progress"),
status: "in_progress",
keyPrefix: "in-progress",
className: "text-vscode-charts-yellow",
},
{
todos: newTodos.filter((todo) => todo.status === "completed"),
status: "completed",
keyPrefix: "completed",
},
]
} else {
// For updates, only show changes
const completedTodos = newTodos.filter((newTodo) => {
if (newTodo.status !== "completed") return false
const previousTodo = previousTodos.find((p) => p.id === newTodo.id || p.content === newTodo.content)
return !previousTodo || previousTodo.status !== "completed"
})
const startedTodos = newTodos.filter((newTodo) => {
if (newTodo.status !== "in_progress") return false
const previousTodo = previousTodos.find((p) => p.id === newTodo.id || p.content === newTodo.content)
return !previousTodo || previousTodo.status !== "in_progress"
})
todoGroups = [
{
todos: completedTodos,
status: "completed",
keyPrefix: "completed",
},
{
todos: startedTodos,
status: "in_progress",
keyPrefix: "started",
className: "text-vscode-charts-yellow",
},
]
}
// If no todos to display, don't render anything
if (todoGroups.every((group) => group.todos.length === 0)) {
return null
}
return (
<div data-todo-changes className="overflow-hidden">
<div className="flex items-center gap-2">
<ListChecks className="size-4 shrink-0" />
<span className="flex-1 overflow-hidden text-ellipsis whitespace-nowrap text-base font-semibold">
{t("chat:todo.updated")}
</span>
</div>
<div className="pl-1 pr-1 pt-1 font-light leading-normal">
{todoGroups.map((group, index) => (
<TodoList key={index} {...group} />
))}
</div>
</div>
)
}

View file

@ -1,5 +1,21 @@
import { cn } from "@/lib/utils"
import { t } from "i18next"
import { ArrowRight, Check, ListChecks, SquareDashed } from "lucide-react"
import { useState, useRef, useMemo, useEffect } from "react"
type TodoStatus = "completed" | "in_progress" | "pending"
function getTodoIcon(status: TodoStatus | null) {
switch (status) {
case "completed":
return <Check className={`size-3 mt-1 shrink-0`} />
case "in_progress":
return <ArrowRight className="size-3 mt-1 shrink-0" />
default:
return <SquareDashed className="size-3 mt-1 shrink-0" />
}
}
export function TodoListDisplay({ todos }: { todos: any[] }) {
const [isCollapsed, setIsCollapsed] = useState(true)
const ulRef = useRef<HTMLUListElement>(null)
@ -37,317 +53,50 @@ export function TodoListDisplay({ todos }: { todos: any[] }) {
const allCompleted = completedCount === totalCount && totalCount > 0
// Create the status icon for the most important todo
const getMostImportantTodoIcon = () => {
if (allCompleted) {
return (
<span
style={{
display: "inline-block",
width: 8,
height: 8,
borderRadius: "50%",
background: "var(--vscode-charts-green)",
marginRight: 8,
marginLeft: 2,
flexShrink: 0,
}}
/>
)
}
if (!mostImportantTodo) {
return (
<span
className="codicon codicon-checklist"
style={{
color: "var(--vscode-foreground)",
marginRight: 8,
marginLeft: 2,
flexShrink: 0,
fontSize: 14,
}}
/>
)
}
if (mostImportantTodo.status === "completed") {
return (
<span
style={{
display: "inline-block",
width: 8,
height: 8,
borderRadius: "50%",
background: "var(--vscode-charts-green)",
marginRight: 8,
marginLeft: 2,
flexShrink: 0,
}}
/>
)
}
if (mostImportantTodo.status === "in_progress") {
return (
<span
style={{
display: "inline-block",
width: 8,
height: 8,
borderRadius: "50%",
background: "var(--vscode-charts-yellow)",
marginRight: 8,
marginLeft: 2,
flexShrink: 0,
}}
/>
)
}
// Default not-started todo
return (
<span
style={{
display: "inline-block",
width: 8,
height: 8,
borderRadius: "50%",
border: "1px solid var(--vscode-descriptionForeground)",
background: "transparent",
marginRight: 8,
marginLeft: 2,
flexShrink: 0,
}}
/>
)
}
return (
<div
className="border border-t-0 rounded-b-xs relative"
style={{
margin: "0",
padding: "6px 10px",
background: "var(--vscode-editor-background,transparent)",
borderColor: "var(--vscode-panel-border)",
}}>
<div data-todo-list className="mt-1 -mx-2.5 border-t border-vscode-sideBar-background overflow-hidden">
<div
style={{
display: "flex",
alignItems: "center",
gap: 2,
marginBottom: 0,
cursor: "pointer",
userSelect: "none",
}}
className={cn(
"flex items-center gap-2 pt-2 px-2.5 cursor-pointer select-none",
mostImportantTodo?.status === "in_progress" && isCollapsed
? "text-vscode-charts-yellow"
: "text-vscode-foreground",
)}
onClick={() => setIsCollapsed((v) => !v)}>
{getMostImportantTodoIcon()}
<span
style={{
fontWeight: 500,
color: allCompleted
? "var(--vscode-charts-green)"
: mostImportantTodo?.status === "in_progress"
? "var(--vscode-charts-yellow)"
: "var(--vscode-foreground)",
flex: 1,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}>
{allCompleted ? "All tasks completed!" : mostImportantTodo?.content || "No pending tasks"}
<ListChecks className="size-3 shrink-0" />
<span className="flex-1 overflow-hidden text-ellipsis whitespace-nowrap">
{isCollapsed
? allCompleted
? t("chat:todo.complete", { total: completedCount })
: mostImportantTodo?.content // show current todo while not done
: t("chat:todo.partial", { completed: completedCount, total: totalCount })}
</span>
<div style={{ display: "flex", alignItems: "center", gap: 4, flexShrink: 0 }}>
<span
className="codicon codicon-checklist"
style={{
color: "var(--vscode-descriptionForeground)",
fontSize: 12,
}}
/>
<span
style={{
color: "var(--vscode-descriptionForeground)",
fontSize: 12,
fontWeight: 500,
}}>
{isCollapsed && completedCount < totalCount && (
<div className="shrink-0 text-vscode-descriptionForeground text-xs">
{completedCount}/{totalCount}
</span>
</div>
</div>
{/* Floating panel for expanded state */}
{!isCollapsed && (
<>
{/* Backdrop */}
<div
style={{
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
background: "rgba(0, 0, 0, 0.1)",
zIndex: 1000,
}}
onClick={() => setIsCollapsed(true)}
/>
{/* Floating panel */}
<div
style={{
position: "absolute",
top: "100%",
left: 0,
right: 0,
marginTop: 4,
background: "var(--vscode-editor-background)",
border: "1px solid var(--vscode-panel-border)",
borderRadius: 6,
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.15)",
zIndex: 1001,
maxHeight: "400px",
minHeight: "200px",
overflow: "hidden",
}}>
{/* Panel header */}
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: "12px 16px",
borderBottom: "1px solid var(--vscode-panel-border)",
background: "var(--vscode-editor-background)",
}}>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<span
className="codicon codicon-checklist"
style={{ color: "var(--vscode-foreground)" }}
/>
<span style={{ fontWeight: "bold", fontSize: 14 }}>Todo List</span>
<span
style={{
color: "var(--vscode-descriptionForeground)",
fontSize: 13,
fontWeight: 500,
}}>
{completedCount}/{totalCount}
</span>
</div>
<span
className="codicon codicon-chevron-up"
style={{
fontSize: 14,
opacity: 0.7,
cursor: "pointer",
padding: "4px",
borderRadius: "2px",
}}
onClick={(e) => {
e.stopPropagation()
setIsCollapsed(true)
}}
onMouseEnter={(e) => {
e.currentTarget.style.opacity = "1"
e.currentTarget.style.background = "var(--vscode-toolbar-hoverBackground)"
}}
onMouseLeave={(e) => {
e.currentTarget.style.opacity = "0.7"
e.currentTarget.style.background = "transparent"
}}
/>
</div>
{/* Todo list */}
<ul
ref={ulRef}
style={{
margin: 0,
paddingLeft: 0,
listStyle: "none",
maxHeight: "340px",
overflowY: "auto",
padding: "12px 16px",
}}>
{todos.map((todo: any, idx: number) => {
let icon
if (todo.status === "completed") {
icon = (
<span
style={{
display: "inline-block",
width: 8,
height: 8,
borderRadius: "50%",
background: "var(--vscode-charts-green)",
marginRight: 8,
marginTop: 7,
flexShrink: 0,
}}
/>
)
} else if (todo.status === "in_progress") {
icon = (
<span
style={{
display: "inline-block",
width: 8,
height: 8,
borderRadius: "50%",
background: "var(--vscode-charts-yellow)",
marginRight: 8,
marginTop: 7,
flexShrink: 0,
}}
/>
)
} else {
icon = (
<span
style={{
display: "inline-block",
width: 8,
height: 8,
borderRadius: "50%",
border: "1px solid var(--vscode-descriptionForeground)",
background: "transparent",
marginRight: 8,
marginTop: 7,
flexShrink: 0,
}}
/>
)
}
return (
<li
key={todo.id || todo.content}
ref={(el) => (itemRefs.current[idx] = el)}
style={{
marginBottom: 8,
display: "flex",
alignItems: "flex-start",
minHeight: 20,
lineHeight: "1.4",
}}>
{icon}
<span
style={{
fontWeight: 500,
color:
todo.status === "completed"
? "var(--vscode-charts-green)"
: todo.status === "in_progress"
? "var(--vscode-charts-yellow)"
: "var(--vscode-foreground)",
wordBreak: "break-word",
}}>
{todo.content}
</span>
</li>
)
})}
</ul>
</div>
</>
)}
</div>
{/* Inline expanded list */}
{!isCollapsed && (
<ul ref={ulRef} className="list-none max-h-[300px] overflow-y-auto mt-2 -mb-1 pb-0 px-2 cursor-default">
{todos.map((todo: any, idx: number) => {
const icon = getTodoIcon(todo.status as TodoStatus)
return (
<li
key={todo.id || todo.content}
ref={(el) => (itemRefs.current[idx] = el)}
className={cn(
"font-light flex flex-row gap-2 items-start min-h-[20px] leading-normal mb-2",
todo.status === "in_progress" && "text-vscode-charts-yellow",
todo.status !== "in_progress" && todo.status !== "completed" && "opacity-60",
)}>
{icon}
<span>{todo.content}</span>
</li>
)
})}
</ul>
)}
</div>
)

View file

@ -423,5 +423,12 @@
"terminal": "Terminal",
"url": "Enganxa la URL per obtenir-ne el contingut"
},
"docs": "Check our <DocsLink>docs</DocsLink> to learn more."
"docs": "Check our <DocsLink>docs</DocsLink> to learn more.",
"todo": {
"partial": "{{completed}} de {{total}} tasques pendents fetes",
"complete": "{{total}} tasques pendents fetes",
"updated": "S'ha actualitzat la llista de tasques pendents",
"completed": "Completat",
"started": "Iniciat"
}
}

View file

@ -423,5 +423,12 @@
"wantsToRun": "Roo möchte einen Slash-Befehl ausführen",
"didRun": "Roo hat einen Slash-Befehl ausgeführt"
},
"docs": "Check our <DocsLink>docs</DocsLink> to learn more."
"docs": "Check our <DocsLink>docs</DocsLink> to learn more.",
"todo": {
"partial": "{{completed}} von {{total}} To-Dos erledigt",
"complete": "{{total}} To-Dos erledigt",
"updated": "Die To-Do-Liste wurde aktualisiert",
"completed": "Abgeschlossen",
"started": "Gestartet"
}
}

View file

@ -414,5 +414,12 @@
"problems": "Problems",
"terminal": "Terminal",
"url": "Paste URL to fetch contents"
},
"todo": {
"partial": "{{completed}} of {{total}} to-dos done",
"complete": "{{total}} to-dos done",
"updated": "Updated the to-do list",
"completed": "Completed",
"started": "Started"
}
}

View file

@ -423,5 +423,12 @@
"wantsToRun": "Roo quiere ejecutar un comando slash",
"didRun": "Roo ejecutó un comando slash"
},
"docs": "Check our <DocsLink>docs</DocsLink> to learn more."
"docs": "Check our <DocsLink>docs</DocsLink> to learn more.",
"todo": {
"partial": "{{completed}} de {{total}} tareas pendientes realizadas",
"complete": "{{total}} tareas pendientes realizadas",
"updated": "Se actualizó la lista de tareas pendientes",
"completed": "Completado",
"started": "Iniciado"
}
}

View file

@ -423,5 +423,12 @@
"wantsToRun": "Roo veut exécuter une commande slash",
"didRun": "Roo a exécuté une commande slash"
},
"docs": "Check our <DocsLink>docs</DocsLink> to learn more."
"docs": "Check our <DocsLink>docs</DocsLink> to learn more.",
"todo": {
"partial": "{{completed}} sur {{total}} tâches terminées",
"complete": "{{total}} tâches terminées",
"updated": "La liste des tâches a été mise à jour",
"completed": "Terminé",
"started": "Commencé"
}
}

View file

@ -423,5 +423,12 @@
"wantsToRun": "Roo एक स्लैश कमांड चलाना चाहता है",
"didRun": "Roo ने एक स्लैश कमांड चलाया"
},
"docs": "Check our <DocsLink>docs</DocsLink> to learn more."
"docs": "Check our <DocsLink>docs</DocsLink> to learn more.",
"todo": {
"partial": "{{total}} में से {{completed}} टू-डू हो गए",
"complete": "{{total}} टू-डू हो गए",
"updated": "टू-डू सूची अपडेट की गई",
"completed": "पूरा हुआ",
"started": "शुरू हुआ"
}
}

View file

@ -429,5 +429,12 @@
"wantsToRun": "Roo ingin menjalankan perintah slash",
"didRun": "Roo telah menjalankan perintah slash"
},
"docs": "Check our <DocsLink>docs</DocsLink> to learn more."
"docs": "Check our <DocsLink>docs</DocsLink> to learn more.",
"todo": {
"partial": "{{completed}} dari {{total}} to-do selesai",
"complete": "{{total}} to-do selesai",
"updated": "Memperbarui daftar to-do",
"completed": "Selesai",
"started": "Dimulai"
}
}

View file

@ -423,5 +423,12 @@
"wantsToRun": "Roo vuole eseguire un comando slash",
"didRun": "Roo ha eseguito un comando slash"
},
"docs": "Check our <DocsLink>docs</DocsLink> to learn more."
"docs": "Check our <DocsLink>docs</DocsLink> to learn more.",
"todo": {
"partial": "{{completed}} di {{total}} cose da fare completate",
"complete": "{{total}} cose da fare completate",
"updated": "Aggiornata la lista delle cose da fare",
"completed": "Completato",
"started": "Iniziato"
}
}

View file

@ -423,5 +423,12 @@
"wantsToRun": "Rooはスラッシュコマンドを実行したい",
"didRun": "Rooはスラッシュコマンドを実行しました"
},
"docs": "Check our <DocsLink>docs</DocsLink> to learn more."
"docs": "Check our <DocsLink>docs</DocsLink> to learn more.",
"todo": {
"partial": "{{total}}件中{{completed}}件のTo-Doが完了",
"complete": "{{total}}件のTo-Doが完了",
"updated": "To-Doリストを更新しました",
"completed": "完了",
"started": "開始"
}
}

View file

@ -423,5 +423,12 @@
"wantsToRun": "Roo가 슬래시 명령어를 실행하려고 합니다",
"didRun": "Roo가 슬래시 명령어를 실행했습니다"
},
"docs": "Check our <DocsLink>docs</DocsLink> to learn more."
"docs": "Check our <DocsLink>docs</DocsLink> to learn more.",
"todo": {
"partial": "{{total}}개의 할 일 중 {{completed}}개 완료",
"complete": "{{total}}개의 할 일 완료",
"updated": "할 일 목록을 업데이트했습니다",
"completed": "완료됨",
"started": "시작됨"
}
}

View file

@ -423,5 +423,12 @@
"wantsToRun": "Roo wil een slash commando uitvoeren",
"didRun": "Roo heeft een slash commando uitgevoerd"
},
"docs": "Check our <DocsLink>docs</DocsLink> to learn more."
"docs": "Check our <DocsLink>docs</DocsLink> to learn more.",
"todo": {
"partial": "{{completed}} van {{total}} to-do's voltooid",
"complete": "{{total}} to-do's voltooid",
"updated": "De to-do-lijst is bijgewerkt",
"completed": "Voltooid",
"started": "Gestart"
}
}

View file

@ -423,5 +423,12 @@
"wantsToRun": "Roo chce uruchomić komendę slash",
"didRun": "Roo uruchomił komendę slash"
},
"docs": "Check our <DocsLink>docs</DocsLink> to learn more."
"docs": "Check our <DocsLink>docs</DocsLink> to learn more.",
"todo": {
"partial": "Ukończono {{completed}} z {{total}} zadań do wykonania",
"complete": "Ukończono {{total}} zadań do wykonania",
"updated": "Zaktualizowano listę zadań do wykonania",
"completed": "Ukończono",
"started": "Rozpoczęto"
}
}

View file

@ -423,5 +423,12 @@
"wantsToRun": "Roo quer executar um comando slash",
"didRun": "Roo executou um comando slash"
},
"docs": "Check our <DocsLink>docs</DocsLink> to learn more."
"docs": "Check our <DocsLink>docs</DocsLink> to learn more.",
"todo": {
"partial": "{{completed}} de {{total}} tarefas concluídas",
"complete": "{{total}} tarefas concluídas",
"updated": "A lista de tarefas foi atualizada",
"completed": "Concluído",
"started": "Iniciado"
}
}

View file

@ -424,5 +424,12 @@
"wantsToRun": "Roo хочет выполнить слеш-команду",
"didRun": "Roo выполнил слеш-команду"
},
"docs": "Check our <DocsLink>docs</DocsLink> to learn more."
"docs": "Check our <DocsLink>docs</DocsLink> to learn more.",
"todo": {
"partial": "{{completed}} из {{total}} задач выполнено",
"complete": "{{total}} задач выполнено",
"updated": "Список задач обновлен",
"completed": "Завершено",
"started": "Начато"
}
}

View file

@ -424,5 +424,12 @@
"wantsToRun": "Roo bir slash komutu çalıştırmak istiyor",
"didRun": "Roo bir slash komutu çalıştırdı"
},
"docs": "Check our <DocsLink>docs</DocsLink> to learn more."
"docs": "Check our <DocsLink>docs</DocsLink> to learn more.",
"todo": {
"partial": "{{total}} yapılacaklar listesinden {{completed}} tanesi tamamlandı",
"complete": "{{total}} yapılacaklar listesi tamamlandı",
"updated": "Yapılacaklar listesi güncellendi",
"completed": "Tamamlandı",
"started": "Başladı"
}
}

View file

@ -424,5 +424,12 @@
"wantsToRun": "Roo muốn chạy lệnh slash",
"didRun": "Roo đã chạy lệnh slash"
},
"docs": "Check our <DocsLink>docs</DocsLink> to learn more."
"docs": "Check our <DocsLink>docs</DocsLink> to learn more.",
"todo": {
"partial": "{{completed}} trong tổng số {{total}} công việc đã hoàn thành",
"complete": "{{total}} công việc đã hoàn thành",
"updated": "Đã cập nhật danh sách công việc",
"completed": "Đã hoàn thành",
"started": "Đã bắt đầu"
}
}

View file

@ -424,5 +424,12 @@
"wantsToRun": "Roo 想要运行斜杠命令",
"didRun": "Roo 运行了斜杠命令"
},
"docs": "Check our <DocsLink>docs</DocsLink> to learn more."
"docs": "Check our <DocsLink>docs</DocsLink> to learn more.",
"todo": {
"partial": "已完成 {{completed}} / {{total}} 个待办事项",
"complete": "已完成 {{total}} 个待办事项",
"updated": "已更新待办事项列表",
"completed": "已完成",
"started": "已开始"
}
}

View file

@ -424,5 +424,12 @@
"wantsToRun": "Roo 想要執行斜線指令",
"didRun": "Roo 執行了斜線指令"
},
"docs": "Check our <DocsLink>docs</DocsLink> to learn more."
"docs": "Check our <DocsLink>docs</DocsLink> to learn more.",
"todo": {
"partial": "已完成 {{completed}} / {{total}} 個待辦事項",
"complete": "已完成 {{total}} 個待辦事項",
"updated": "已更新待辦事項列表",
"completed": "已完成",
"started": "已開始"
}
}

View file

@ -388,14 +388,18 @@ vscode-dropdown::part(listbox) {
}
.mention-context-highlight {
background-color: color-mix(in srgb, var(--vscode-badge-foreground) 30%, transparent);
border-radius: 3px;
background-color: color-mix(in srgb, var(--vscode-badge-foreground) 15%, transparent);
border-radius: 6px;
padding: 1px 4px;
font-family: var(--font-mono);
}
.mention-context-highlight-with-shadow {
background-color: color-mix(in srgb, var(--vscode-badge-foreground) 30%, transparent);
background-color: color-mix(in srgb, var(--vscode-badge-foreground) 15%, transparent);
border-radius: 3px;
padding: 1px 4px;
box-shadow: 0 0 0 0.5px color-mix(in srgb, var(--vscode-badge-foreground) 30%, transparent);
font-family: var(--font-mono);
}
/**