Skip to content

Commit fff5cf8

Browse files
brunobergherroomote[bot]mrubens
authored
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 <[email protected]>
1 parent fc2147a commit fff5cf8

28 files changed

+536
-447
lines changed

webview-ui/src/components/chat/ChatRow.tsx

Lines changed: 40 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { formatPathTooltip } from "@src/utils/formatPathTooltip"
1818

1919
import { ToolUseBlock, ToolUseBlockHeader } from "../common/ToolUseBlock"
2020
import UpdateTodoListToolBlock from "./UpdateTodoListToolBlock"
21+
import { TodoChangeDisplay } from "./TodoChangeDisplay"
2122
import CodeAccordian from "../common/CodeAccordian"
2223
import MarkdownBlock from "../common/MarkdownBlock"
2324
import { ReasoningBlock } from "./ReasoningBlock"
@@ -62,6 +63,39 @@ import {
6263
import { cn } from "@/lib/utils"
6364
import { PathTooltip } from "../ui/PathTooltip"
6465

66+
// Helper function to get previous todos before a specific message
67+
function getPreviousTodos(messages: ClineMessage[], currentMessageTs: number): any[] {
68+
// Find the previous updateTodoList message before the current one
69+
const previousUpdateIndex = messages
70+
.slice()
71+
.reverse()
72+
.findIndex((msg) => {
73+
if (msg.ts >= currentMessageTs) return false
74+
if (msg.type === "ask" && msg.ask === "tool") {
75+
try {
76+
const tool = JSON.parse(msg.text || "{}")
77+
return tool.tool === "updateTodoList"
78+
} catch {
79+
return false
80+
}
81+
}
82+
return false
83+
})
84+
85+
if (previousUpdateIndex !== -1) {
86+
const previousMessage = messages.slice().reverse()[previousUpdateIndex]
87+
try {
88+
const tool = JSON.parse(previousMessage.text || "{}")
89+
return tool.todos || []
90+
} catch {
91+
return []
92+
}
93+
}
94+
95+
// If no previous updateTodoList message, return empty array
96+
return []
97+
}
98+
6599
interface ChatRowProps {
66100
message: ClineMessage
67101
lastModifiedMessage?: ClineMessage
@@ -127,11 +161,10 @@ export const ChatRowContent = ({
127161
onFollowUpUnmount,
128162
onBatchFileResponse,
129163
isFollowUpAnswered,
130-
editable,
131164
}: ChatRowContentProps) => {
132165
const { t } = useTranslation()
133166

134-
const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode, apiConfiguration } = useExtensionState()
167+
const { mcpServers, alwaysAllowMcp, currentCheckpoint, mode, apiConfiguration, clineMessages } = useExtensionState()
135168
const { info: model } = useSelectedModel(apiConfiguration)
136169
const [isEditing, setIsEditing] = useState(false)
137170
const [editedContent, setEditedContent] = useState("")
@@ -503,18 +536,11 @@ export const ChatRowContent = ({
503536
}
504537
case "updateTodoList" as any: {
505538
const todos = (tool as any).todos || []
506-
return (
507-
<UpdateTodoListToolBlock
508-
todos={todos}
509-
content={tool.content}
510-
onChange={(updatedTodos) => {
511-
if (typeof vscode !== "undefined" && vscode?.postMessage) {
512-
vscode.postMessage({ type: "updateTodoList", payload: { todos: updatedTodos } })
513-
}
514-
}}
515-
editable={editable && isLast}
516-
/>
517-
)
539+
540+
// Get previous todos from the latest todos in the task context
541+
const previousTodos = getPreviousTodos(clineMessages, message.ts)
542+
543+
return <TodoChangeDisplay previousTodos={previousTodos} newTodos={todos} />
518544
}
519545
case "newFileCreated":
520546
return (

webview-ui/src/components/chat/CloudTaskButton.tsx

Lines changed: 9 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import { useState, useEffect, useCallback } from "react"
22
import { useTranslation } from "react-i18next"
3-
import { CloudUpload, Copy, Check } from "lucide-react"
3+
import { Copy, Check, CloudUploadIcon } from "lucide-react"
44
import QRCode from "qrcode"
55

66
import type { HistoryItem } from "@roo-code/types"
77

88
import { useExtensionState } from "@/context/ExtensionStateContext"
99
import { useCopyToClipboard } from "@/utils/clipboard"
10-
import { Button, Dialog, DialogContent, DialogHeader, DialogTitle, Input, StandardTooltip } from "@/components/ui"
10+
import { Button, Dialog, DialogContent, DialogHeader, DialogTitle, Input } from "@/components/ui"
1111
import { vscode } from "@/utils/vscode"
12+
import { LucideIconButton } from "./LucideIconButton"
1213

1314
interface CloudTaskButtonProps {
1415
item?: HistoryItem
@@ -83,18 +84,12 @@ export const CloudTaskButton = ({ item, disabled = false }: CloudTaskButtonProps
8384

8485
return (
8586
<>
86-
<StandardTooltip content={t("chat:task.openInCloud")}>
87-
<Button
88-
variant="ghost"
89-
size="icon"
90-
disabled={disabled}
91-
className="h-7 w-7 p-1.5 hover:bg-vscode-toolbar-hoverBackground"
92-
onClick={() => setDialogOpen(true)}
93-
data-testid="cloud-task-button"
94-
aria-label={t("chat:task.openInCloud")}>
95-
<CloudUpload className="h-4 w-4" />
96-
</Button>
97-
</StandardTooltip>
87+
<LucideIconButton
88+
icon={CloudUploadIcon}
89+
disabled={disabled}
90+
data-testid="cloud-task-button"
91+
title={t("chat:task.openInCloud")}
92+
onClick={() => setDialogOpen(true)}></LucideIconButton>
9893

9994
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
10095
<DialogContent className="max-w-100">
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { cn } from "@src/lib/utils"
2+
import { Button, StandardTooltip } from "@src/components/ui"
3+
import { Loader2, LucideIcon } from "lucide-react"
4+
5+
interface LucideIconButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
6+
icon: LucideIcon
7+
title: string
8+
disabled?: boolean
9+
tooltip?: boolean
10+
isLoading?: boolean
11+
style?: React.CSSProperties
12+
}
13+
14+
export const LucideIconButton: React.FC<LucideIconButtonProps> = ({
15+
icon,
16+
title,
17+
className,
18+
disabled,
19+
tooltip = true,
20+
isLoading,
21+
onClick,
22+
style,
23+
...props
24+
}) => {
25+
const Icon = icon
26+
return (
27+
<StandardTooltip content={tooltip ? title : undefined}>
28+
<Button
29+
aria-label={title}
30+
className={cn(
31+
"relative inline-flex items-center justify-center",
32+
"bg-transparent border-none p-1.5",
33+
"rounded-full",
34+
"text-vscode-foreground opacity-85",
35+
"transition-all duration-150",
36+
"focus:outline-none focus-visible:ring-1 focus-visible:ring-vscode-focusBorder",
37+
"active:bg-[rgba(255,255,255,0.1)]",
38+
!disabled && "cursor-pointer hover:opacity-100 hover:bg-[rgba(255,255,255,0.03)]",
39+
disabled && "cursor-not-allowed opacity-40 hover:bg-transparent active:bg-transparent",
40+
className,
41+
)}
42+
disabled={disabled}
43+
onClick={!disabled ? onClick : undefined}
44+
style={{ fontSize: 16.5, ...style }}
45+
{...props}>
46+
{isLoading ? <Loader2 className="size-2.5 animate-spin" /> : <Icon className="size-2.5" />}
47+
</Button>
48+
</StandardTooltip>
49+
)
50+
}

webview-ui/src/components/chat/Mention.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const Mention = ({ text, withShadow = false }: MentionProps) => {
2121
return (
2222
<span
2323
key={index}
24-
className={`${withShadow ? "mention-context-highlight-with-shadow" : "mention-context-highlight"} cursor-pointer`}
24+
className={`${withShadow ? "mention-context-highlight-with-shadow" : "mention-context-highlight"} text-[0.9em] cursor-pointer`}
2525
onClick={() => vscode.postMessage({ type: "openMention", text: part })}>
2626
@{part}
2727
</span>

webview-ui/src/components/chat/ShareButton.tsx

Lines changed: 14 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { useState, useEffect } from "react"
22
import { useTranslation } from "react-i18next"
3-
import { Share2 } from "lucide-react"
3+
import { Share2Icon } from "lucide-react"
44

55
import { type HistoryItem, type ShareVisibility, TelemetryEventName } from "@roo-code/types"
66

@@ -10,7 +10,6 @@ import { useExtensionState } from "@/context/ExtensionStateContext"
1010
import { useCloudUpsell } from "@/hooks/useCloudUpsell"
1111
import { CloudUpsellDialog } from "@/components/cloud/CloudUpsellDialog"
1212
import {
13-
Button,
1413
Popover,
1514
PopoverContent,
1615
PopoverTrigger,
@@ -20,14 +19,14 @@ import {
2019
CommandGroup,
2120
StandardTooltip,
2221
} from "@/components/ui"
22+
import { LucideIconButton } from "./LucideIconButton"
2323

2424
interface ShareButtonProps {
2525
item?: HistoryItem
2626
disabled?: boolean
27-
showLabel?: boolean
2827
}
2928

30-
export const ShareButton = ({ item, disabled = false, showLabel = false }: ShareButtonProps) => {
29+
export const ShareButton = ({ item, disabled = false }: ShareButtonProps) => {
3130
const [shareDropdownOpen, setShareDropdownOpen] = useState(false)
3231
const [shareSuccess, setShareSuccess] = useState<{ visibility: ShareVisibility; url: string } | null>(null)
3332
const [wasConnectInitiatedFromShare, setWasConnectInitiatedFromShare] = useState(false)
@@ -153,20 +152,13 @@ export const ShareButton = ({ item, disabled = false, showLabel = false }: Share
153152
<Popover open={shareDropdownOpen} onOpenChange={setShareDropdownOpen}>
154153
<StandardTooltip content={shareButtonState.title}>
155154
<PopoverTrigger asChild>
156-
<Button
157-
variant="ghost"
158-
size={showLabel ? "sm" : "icon"}
155+
<LucideIconButton
156+
icon={Share2Icon}
159157
disabled={disabled || shareButtonState.disabled}
160-
className={
161-
showLabel
162-
? "h-7 px-2 hover:bg-vscode-toolbar-hoverBackground"
163-
: "h-7 w-7 p-1.5 hover:bg-vscode-toolbar-hoverBackground"
164-
}
158+
tooltip={false}
165159
onClick={handleShareButtonClick}
166-
data-testid="share-button">
167-
<Share2 />
168-
{showLabel && <span className="ml-0">{t("chat:task.share")}</span>}
169-
</Button>
160+
data-testid="share-button"
161+
title={t("chat:task.share")}></LucideIconButton>
170162
</PopoverTrigger>
171163
</StandardTooltip>
172164

@@ -221,22 +213,12 @@ export const ShareButton = ({ item, disabled = false, showLabel = false }: Share
221213
</PopoverContent>
222214
</Popover>
223215
) : (
224-
<StandardTooltip content={shareButtonState.title}>
225-
<Button
226-
variant="ghost"
227-
size={showLabel ? "sm" : "icon"}
228-
disabled={disabled || shareButtonState.disabled}
229-
className={
230-
showLabel
231-
? "h-7 px-2 hover:bg-vscode-toolbar-hoverBackground"
232-
: "h-7 w-7 p-1.5 hover:bg-vscode-toolbar-hoverBackground"
233-
}
234-
onClick={handleShareButtonClick}
235-
data-testid="share-button">
236-
<Share2 />
237-
{showLabel && <span className="ml-1">{t("chat:task.share")}</span>}
238-
</Button>
239-
</StandardTooltip>
216+
<LucideIconButton
217+
icon={Share2Icon}
218+
disabled={disabled || shareButtonState.disabled}
219+
title={shareButtonState.title}
220+
onClick={handleShareButtonClick}
221+
data-testid="share-button"></LucideIconButton>
240222
)}
241223

242224
{/* Connect to Cloud Modal */}

webview-ui/src/components/chat/TaskActions.tsx

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@ import { vscode } from "@/utils/vscode"
77
import { useCopyToClipboard } from "@/utils/clipboard"
88

99
import { DeleteTaskDialog } from "../history/DeleteTaskDialog"
10-
import { IconButton } from "./IconButton"
1110
import { ShareButton } from "./ShareButton"
1211
import { CloudTaskButton } from "./CloudTaskButton"
12+
import { CopyIcon, DownloadIcon, Trash2Icon } from "lucide-react"
13+
import { LucideIconButton } from "./LucideIconButton"
1314

1415
interface TaskActionsProps {
1516
item?: HistoryItem
@@ -19,40 +20,38 @@ interface TaskActionsProps {
1920
export const TaskActions = ({ item, buttonsDisabled }: TaskActionsProps) => {
2021
const [deleteTaskId, setDeleteTaskId] = useState<string | null>(null)
2122
const { t } = useTranslation()
22-
const { copyWithFeedback, showCopyFeedback } = useCopyToClipboard()
23+
const { copyWithFeedback } = useCopyToClipboard()
2324

2425
return (
25-
<div className="flex flex-row items-center">
26-
<IconButton
27-
iconClass="codicon-desktop-download"
26+
<div className="flex flex-row items-center -ml-0.5 mt-1 gap-1">
27+
<LucideIconButton
28+
icon={DownloadIcon}
2829
title={t("chat:task.export")}
2930
onClick={() => vscode.postMessage({ type: "exportCurrentTask" })}
3031
/>
32+
3133
{item?.task && (
32-
<IconButton
33-
iconClass={showCopyFeedback ? "codicon-check" : "codicon-copy"}
34+
<LucideIconButton
35+
icon={CopyIcon}
3436
title={t("history:copyPrompt")}
3537
onClick={(e) => copyWithFeedback(item.task, e)}
3638
/>
3739
)}
3840
{!!item?.size && item.size > 0 && (
3941
<>
40-
<div className="flex items-center">
41-
<IconButton
42-
iconClass="codicon-trash"
43-
title={t("chat:task.delete")}
44-
disabled={buttonsDisabled}
45-
onClick={(e) => {
46-
e.stopPropagation()
47-
48-
if (e.shiftKey) {
49-
vscode.postMessage({ type: "deleteTaskWithId", text: item.id })
50-
} else {
51-
setDeleteTaskId(item.id)
52-
}
53-
}}
54-
/>
55-
</div>
42+
<LucideIconButton
43+
icon={Trash2Icon}
44+
title={t("chat:task.delete")}
45+
disabled={buttonsDisabled}
46+
onClick={(e) => {
47+
e.stopPropagation()
48+
if (e.shiftKey) {
49+
vscode.postMessage({ type: "deleteTaskWithId", text: item.id })
50+
} else {
51+
setDeleteTaskId(item.id)
52+
}
53+
}}
54+
/>
5655
{deleteTaskId && (
5756
<DeleteTaskDialog
5857
taskId={deleteTaskId}
@@ -62,7 +61,7 @@ export const TaskActions = ({ item, buttonsDisabled }: TaskActionsProps) => {
6261
)}
6362
</>
6463
)}
65-
<ShareButton item={item} disabled={false} showLabel={false} />
64+
<ShareButton item={item} disabled={false} />
6665
<CloudTaskButton item={item} disabled={buttonsDisabled} />
6766
</div>
6867
)

0 commit comments

Comments
 (0)