Redesigned Task Header (#6561)

* Reorganizes the task header for cleanliness and in preparation for Cloud link

* More task ehader visual tweaks

* Translations for new task header

* Fixes TaskHeader color

* Removes stray string

* Fixes tests

* Iterates on visual details

* More visual tweaks

* Missing localization call

* Fixes tests

---------

Co-authored-by: Bruno Bergher <me@brunobergher.comexport>
This commit is contained in:
Bruno Bergher 2025-08-05 14:26:25 +01:00 committed by GitHub
parent d90bab71ff
commit ea79dfeb66
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
27 changed files with 384 additions and 273 deletions

View file

@ -1,6 +1,6 @@
// npm run test ContextWindowProgress.spec.tsx
import { render, screen } from "@/utils/test-utils"
import { render, screen, fireEvent } from "@/utils/test-utils"
import { QueryClient, QueryClientProvider } from "@tanstack/react-query"
import TaskHeader from "@src/components/chat/TaskHeader"
@ -70,6 +70,10 @@ describe("ContextWindowProgress", () => {
it("renders correctly with valid inputs", () => {
renderComponent({ contextTokens: 1000, contextWindow: 4000 })
// First expand the TaskHeader to access ContextWindowProgress
const taskHeader = screen.getByText("Test task")
fireEvent.click(taskHeader)
// Check for basic elements
// The context-window-label is not part of the ContextWindowProgress component
// but rather part of the parent TaskHeader component in expanded state
@ -83,6 +87,10 @@ describe("ContextWindowProgress", () => {
it("handles zero context window gracefully", () => {
renderComponent({ contextTokens: 0, contextWindow: 0 })
// First expand the TaskHeader to access ContextWindowProgress
const taskHeader = screen.getByText("Test task")
fireEvent.click(taskHeader)
// In the current implementation, the component is still displayed with zero values
// rather than being hidden completely
// The context-window-label is not part of the ContextWindowProgress component
@ -93,6 +101,10 @@ describe("ContextWindowProgress", () => {
it("handles edge cases with negative values", () => {
renderComponent({ contextTokens: -100, contextWindow: 4000 })
// First expand the TaskHeader to access ContextWindowProgress
const taskHeader = screen.getByText("Test task")
fireEvent.click(taskHeader)
// Should show 0 instead of -100
expect(screen.getByTestId("context-tokens-count")).toHaveTextContent("0")
// The actual context window might be different than what we pass in
@ -102,6 +114,10 @@ describe("ContextWindowProgress", () => {
it("calculates percentages correctly", () => {
renderComponent({ contextTokens: 1000, contextWindow: 4000 })
// First expand the TaskHeader to access ContextWindowProgress
const taskHeader = screen.getByText("Test task")
fireEvent.click(taskHeader)
// Verify that the token count and window size are displayed correctly
const tokenCount = screen.getByTestId("context-tokens-count")
const windowSize = screen.getByTestId("context-window-size")

View file

@ -805,8 +805,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
[clineAsk, startNewTask, isStreaming],
)
const handleTaskCloseButtonClick = useCallback(() => startNewTask(), [startNewTask])
const { info: model } = useSelectedModel(apiConfiguration)
const selectImages = useCallback(() => vscode.postMessage({ type: "selectImages" }), [])
@ -1765,7 +1763,6 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
contextTokens={apiMetrics.contextTokens}
buttonsDisabled={sendingDisabled}
handleCondenseContext={handleCondenseContext}
onClose={handleTaskCloseButtonClick}
todos={latestTodos}
/>

View file

@ -55,7 +55,7 @@ export const ContextWindowProgress = ({ contextWindow, contextTokens, maxTokens
return (
<>
<div className="flex items-center gap-2 flex-1 whitespace-nowrap px-2">
<div className="flex items-center gap-2 flex-1 whitespace-nowrap">
<div data-testid="context-tokens-count">{formatLargeNumber(safeContextTokens)}</div>
<StandardTooltip content={tooltipContent} side="top" sideOffset={8}>
<div className="flex-1 relative">

View file

@ -1,5 +1,6 @@
import { useState, useEffect, useRef } from "react"
import { useTranslation } from "react-i18next"
import { SquareArrowOutUpRightIcon } from "lucide-react"
import type { HistoryItem, ShareVisibility } from "@roo-code/types"
import { TelemetryEventName } from "@roo-code/types"
@ -26,9 +27,10 @@ import {
interface ShareButtonProps {
item?: HistoryItem
disabled?: boolean
showLabel?: boolean
}
export const ShareButton = ({ item, disabled = false }: ShareButtonProps) => {
export const ShareButton = ({ item, disabled = false, showLabel = false }: ShareButtonProps) => {
const [shareDropdownOpen, setShareDropdownOpen] = useState(false)
const [connectModalOpen, setConnectModalOpen] = useState(false)
const [shareSuccess, setShareSuccess] = useState<{ visibility: ShareVisibility; url: string } | null>(null)
@ -155,14 +157,21 @@ export const ShareButton = ({ item, disabled = false }: ShareButtonProps) => {
<PopoverTrigger asChild>
<Button
variant="ghost"
size="icon"
size={showLabel ? "sm" : "icon"}
disabled={disabled || shareButtonState.disabled}
className="h-7 w-7 p-1.5 hover:bg-vscode-toolbar-hoverBackground"
onClick={handleShareButtonClick}>
<span className="codicon codicon-link"></span>
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">
<SquareArrowOutUpRightIcon />
{showLabel && <span className="ml-0">{t("chat:task.share")}</span>}
</Button>
</PopoverTrigger>
</StandardTooltip>
<PopoverContent className="w-56 p-0" align="start">
{shareSuccess ? (
<div className="p-3">
@ -217,11 +226,17 @@ export const ShareButton = ({ item, disabled = false }: ShareButtonProps) => {
<StandardTooltip content={shareButtonState.title}>
<Button
variant="ghost"
size="icon"
size={showLabel ? "sm" : "icon"}
disabled={disabled || shareButtonState.disabled}
className="h-7 w-7 p-1.5 hover:bg-vscode-toolbar-hoverBackground"
onClick={handleShareButtonClick}>
<span className="codicon codicon-link"></span>
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">
<SquareArrowOutUpRightIcon />
{showLabel && <span className="ml-1">{t("chat:task.share")}</span>}
</Button>
</StandardTooltip>
)}

View file

@ -1,5 +1,4 @@
import { useState } from "react"
import prettyBytes from "pretty-bytes"
import { useTranslation } from "react-i18next"
import type { HistoryItem } from "@roo-code/types"
@ -22,8 +21,7 @@ export const TaskActions = ({ item, buttonsDisabled }: TaskActionsProps) => {
const { copyWithFeedback, showCopyFeedback } = useCopyToClipboard()
return (
<div className="flex flex-row gap-1">
<ShareButton item={item} disabled={false} />
<div className="flex flex-row items-center">
<IconButton
iconClass="codicon-desktop-download"
title={t("chat:task.export")}
@ -53,7 +51,6 @@ export const TaskActions = ({ item, buttonsDisabled }: TaskActionsProps) => {
}
}}
/>
<span className="ml-1 text-xs text-vscode-foreground opacity-85">{prettyBytes(item.size)}</span>
</div>
{deleteTaskId && (
<DeleteTaskDialog
@ -64,6 +61,7 @@ export const TaskActions = ({ item, buttonsDisabled }: TaskActionsProps) => {
)}
</>
)}
<ShareButton item={item} disabled={false} showLabel={false} />
</div>
)
}

View file

@ -1,8 +1,7 @@
import { memo, useRef, useState } from "react"
import { useWindowSize } from "react-use"
import { useTranslation } from "react-i18next"
import { VSCodeBadge } from "@vscode/webview-ui-toolkit/react"
import { CloudUpload, CloudDownload, FoldVertical } from "lucide-react"
import { FoldVertical, ChevronUp, ChevronDown } from "lucide-react"
import prettyBytes from "pretty-bytes"
import type { ClineMessage } from "@roo-code/types"
@ -10,14 +9,13 @@ import { getModelMaxOutputTokens } from "@roo/api"
import { formatLargeNumber } from "@src/utils/format"
import { cn } from "@src/lib/utils"
import { Button, StandardTooltip } from "@src/components/ui"
import { StandardTooltip } from "@src/components/ui"
import { useExtensionState } from "@src/context/ExtensionStateContext"
import { useSelectedModel } from "@/components/ui/hooks/useSelectedModel"
import Thumbnails from "../common/Thumbnails"
import { TaskActions } from "./TaskActions"
import { ShareButton } from "./ShareButton"
import { ContextWindowProgress } from "./ContextWindowProgress"
import { Mention } from "./Mention"
import { TodoListDisplay } from "./TodoListDisplay"
@ -32,7 +30,6 @@ export interface TaskHeaderProps {
contextTokens: number
buttonsDisabled: boolean
handleCondenseContext: (taskId: string) => void
onClose: () => void
todos?: any[]
}
@ -46,7 +43,6 @@ const TaskHeader = ({
contextTokens,
buttonsDisabled,
handleCondenseContext,
onClose,
todos,
}: TaskHeaderProps) => {
const { t } = useTranslation()
@ -58,8 +54,6 @@ const TaskHeader = ({
const textRef = useRef<HTMLDivElement>(null)
const contextWindow = model?.contextWindow || 1
const { width: windowWidth } = useWindowSize()
const condenseButton = (
<StandardTooltip content={t("chat:task.condenseContext")}>
<button
@ -74,55 +68,104 @@ const TaskHeader = ({
const hasTodos = todos && Array.isArray(todos) && todos.length > 0
return (
<div className="py-2 px-3">
<div className="pt-2 pb-0 px-3">
<div
className={cn(
"p-2.5 flex flex-col gap-1.5 relative z-1 border",
"px-2.5 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",
hasTodos ? "rounded-t-xs border-b-0" : "rounded-xs",
isTaskExpanded
? "border-vscode-panel-border text-vscode-foreground"
: "border-vscode-panel-border/80 text-vscode-foreground/80",
)}>
<div className="flex justify-between items-center gap-2">
<div
className="flex items-center cursor-pointer -ml-0.5 select-none grow min-w-0"
onClick={() => setIsTaskExpanded(!isTaskExpanded)}>
<div className="flex items-center shrink-0">
<span className={`codicon codicon-chevron-${isTaskExpanded ? "down" : "right"}`}></span>
</div>
<div className="ml-1.5 whitespace-nowrap overflow-hidden text-ellipsis grow min-w-0">
<span className="font-bold">
{t("chat:task.title")}
{!isTaskExpanded && ":"}
</span>
)}
onClick={(e) => {
// Don't expand if clicking on buttons or interactive elements
if (
e.target instanceof Element &&
(e.target.closest("button") ||
e.target.closest('[role="button"]') ||
e.target.closest(".share-button") ||
e.target.closest("[data-radix-popper-content-wrapper]") ||
e.target.closest("img") ||
e.target.tagName === "IMG")
) {
return
}
// Don't expand/collapse if user is selecting text
const selection = window.getSelection()
if (selection && selection.toString().length > 0) {
return
}
setIsTaskExpanded(!isTaskExpanded)
}}>
<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 && (
<span className="ml-1">
<div>
<span className="font-bold mr-1">{t("chat:task.title")}</span>
<Mention text={task.text} />
</span>
</div>
)}
</div>
<div className="flex items-center shrink-0 ml-2" onClick={(e) => e.stopPropagation()}>
<StandardTooltip content={isTaskExpanded ? t("chat:task.collapse") : t("chat:task.expand")}>
<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} />}
</button>
</StandardTooltip>
</div>
</div>
<StandardTooltip content={t("chat:task.closeAndStart")}>
<Button variant="ghost" size="icon" onClick={onClose} className="shrink-0 w-5 h-5">
<span className="codicon codicon-close" />
</Button>
</StandardTooltip>
</div>
{/* Collapsed state: Track context and cost if we have any */}
{!isTaskExpanded && contextWindow > 0 && (
<div className={`w-full flex flex-row items-center gap-1 h-auto`}>
<ContextWindowProgress
contextWindow={contextWindow}
contextTokens={contextTokens || 0}
maxTokens={
model
? getModelMaxOutputTokens({ modelId, model, settings: apiConfiguration })
: undefined
<div className="flex items-center gap-2 text-sm" onClick={(e) => e.stopPropagation()}>
<StandardTooltip
content={
<div className="space-y-1">
<div>
{t("chat:tokenProgress.tokensUsed", {
used: formatLargeNumber(contextTokens || 0),
total: formatLargeNumber(contextWindow),
})}
</div>
{(() => {
const maxTokens = model
? getModelMaxOutputTokens({ modelId, model, settings: apiConfiguration })
: 0
const reservedForOutput = maxTokens || 0
const availableSpace = contextWindow - (contextTokens || 0) - reservedForOutput
return (
<>
{reservedForOutput > 0 && (
<div>
{t("chat:tokenProgress.reservedForResponse", {
amount: formatLargeNumber(reservedForOutput),
})}
</div>
)}
{availableSpace > 0 && (
<div>
{t("chat:tokenProgress.availableSpace", {
amount: formatLargeNumber(availableSpace),
})}
</div>
)}
</>
)
})()}
</div>
}
/>
{condenseButton}
<ShareButton item={currentTaskItem} disabled={buttonsDisabled} />
{!!totalCost && <VSCodeBadge>${totalCost.toFixed(2)}</VSCodeBadge>}
side="top"
sideOffset={8}>
<span className="mr-1">
{formatLargeNumber(contextTokens || 0)} / {formatLargeNumber(contextWindow)}
</span>
</StandardTooltip>
{!!totalCost && <span>${totalCost.toFixed(2)}</span>}
</div>
)}
{/* Expanded state: Show task text and images */}
@ -130,10 +173,10 @@ const TaskHeader = ({
<>
<div
ref={textContainerRef}
className="-mt-0.5 text-vscode-font-size overflow-y-auto break-words break-anywhere relative">
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"
className="overflow-auto max-h-80 whitespace-pre-wrap break-words break-anywhere cursor-text"
style={{
display: "-webkit-box",
WebkitLineClamp: "unset",
@ -144,78 +187,112 @@ const TaskHeader = ({
</div>
{task.images && task.images.length > 0 && <Thumbnails images={task.images} />}
<div className="flex flex-col gap-1">
{isTaskExpanded && contextWindow > 0 && (
<div
className={`w-full flex ${windowWidth < 400 ? "flex-col" : "flex-row"} gap-1 h-auto`}>
<div className="flex items-center gap-1 flex-shrink-0">
<span className="font-bold" data-testid="context-window-label">
{t("chat:task.contextWindow")}
</span>
</div>
<ContextWindowProgress
contextWindow={contextWindow}
contextTokens={contextTokens || 0}
maxTokens={
model
? getModelMaxOutputTokens({
modelId,
model,
settings: apiConfiguration,
})
: undefined
}
/>
{condenseButton}
</div>
)}
<div className="flex justify-between items-center h-[20px]">
<div className="flex items-center gap-1 flex-wrap">
<span className="font-bold">{t("chat:task.tokens")}</span>
{typeof tokensIn === "number" && tokensIn > 0 && (
<span className="flex items-center gap-0.5">
<i className="codicon codicon-arrow-up text-xs font-bold" />
{formatLargeNumber(tokensIn)}
</span>
<div className="border-t border-b border-vscode-panel-border/50 py-4 mt-2 mb-1">
<table className="w-full">
<tbody>
{contextWindow > 0 && (
<tr>
<th
className="font-bold text-left align-top w-1 whitespace-nowrap pl-1 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`}>
<ContextWindowProgress
contextWindow={contextWindow}
contextTokens={contextTokens || 0}
maxTokens={
model
? getModelMaxOutputTokens({
modelId,
model,
settings: apiConfiguration,
})
: undefined
}
/>
{condenseButton}
</div>
</td>
</tr>
)}
{typeof tokensOut === "number" && tokensOut > 0 && (
<span className="flex items-center gap-0.5">
<i className="codicon codicon-arrow-down text-xs font-bold" />
{formatLargeNumber(tokensOut)}
</span>
)}
</div>
{!totalCost && <TaskActions item={currentTaskItem} buttonsDisabled={buttonsDisabled} />}
</div>
{((typeof cacheReads === "number" && cacheReads > 0) ||
(typeof cacheWrites === "number" && cacheWrites > 0)) && (
<div className="flex items-center gap-1 flex-wrap h-[20px]">
<span className="font-bold">{t("chat:task.cache")}</span>
{typeof cacheWrites === "number" && cacheWrites > 0 && (
<span className="flex items-center gap-0.5">
<CloudUpload size={16} />
{formatLargeNumber(cacheWrites)}
</span>
)}
{typeof cacheReads === "number" && cacheReads > 0 && (
<span className="flex items-center gap-0.5">
<CloudDownload size={16} />
{formatLargeNumber(cacheReads)}
</span>
)}
</div>
)}
<tr>
<th className="font-bold text-left align-top w-1 whitespace-nowrap pl-1 pr-3 h-[24px]">
{t("chat:task.tokens")}
</th>
<td className="align-top">
<div className="flex items-center gap-1 flex-wrap">
{typeof tokensIn === "number" && tokensIn > 0 && (
<span> {formatLargeNumber(tokensIn)}</span>
)}
{typeof tokensOut === "number" && tokensOut > 0 && (
<span> {formatLargeNumber(tokensOut)}</span>
)}
</div>
</td>
</tr>
{!!totalCost && (
<div className="flex justify-between items-center h-[20px]">
<div className="flex items-center gap-1">
<span className="font-bold">{t("chat:task.apiCost")}</span>
<span>${totalCost?.toFixed(2)}</span>
</div>
<TaskActions item={currentTaskItem} buttonsDisabled={buttonsDisabled} />
</div>
)}
{((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]">
{t("chat:task.cache")}
</th>
<td className="align-top">
<div className="flex items-center gap-1 flex-wrap">
{typeof cacheWrites === "number" && cacheWrites > 0 && (
<span> {formatLargeNumber(cacheWrites)}</span>
)}
{typeof cacheReads === "number" && cacheReads > 0 && (
<span> {formatLargeNumber(cacheReads)}</span>
)}
</div>
</td>
</tr>
)}
{!!totalCost && (
<tr>
<th className="font-bold text-left align-top w-1 whitespace-nowrap pl-1 pr-3 h-[24px]">
{t("chat:task.apiCost")}
</th>
<td className="align-top">
<span>${totalCost?.toFixed(2)}</span>
</td>
</tr>
)}
{/* Cache size display */}
{((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]">
{t("chat:task.cache")}
</th>
<td className="align-top">
{prettyBytes(((cacheReads || 0) + (cacheWrites || 0)) * 4)}
</td>
</tr>
)}
{/* 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]">
{t("chat:task.size")}
</th>
<td className="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>
</>
)}

View file

@ -89,19 +89,17 @@ describe("TaskActions", () => {
it("renders share button when item has id", () => {
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
// Find button by its icon class
const buttons = screen.getAllByRole("button")
const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link"))
// ShareButton now uses data-testid for reliable testing
const shareButton = screen.getByTestId("share-button")
expect(shareButton).toBeInTheDocument()
})
it("does not render share button when item has no id", () => {
render(<TaskActions item={undefined} buttonsDisabled={false} />)
// Find button by its icon class
const buttons = screen.queryAllByRole("button")
const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link"))
expect(shareButton).not.toBeDefined()
// ShareButton returns null when no item ID
const shareButton = screen.queryByTestId("share-button")
expect(shareButton).toBeNull()
})
it("renders share button even when not authenticated", () => {
@ -112,9 +110,8 @@ describe("TaskActions", () => {
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
// Find button by its icon class
const buttons = screen.getAllByRole("button")
const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link"))
// ShareButton should still render when not authenticated
const shareButton = screen.getByTestId("share-button")
expect(shareButton).toBeInTheDocument()
})
})
@ -123,11 +120,9 @@ describe("TaskActions", () => {
it("shows organization and public share options when authenticated and sharing enabled", () => {
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
// Find button by its icon class
const buttons = screen.getAllByRole("button")
const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link"))
expect(shareButton).toBeDefined()
fireEvent.click(shareButton!)
// Find share button by its test ID and click it
const shareButton = screen.getByTestId("share-button")
fireEvent.click(shareButton)
expect(screen.getByText("Share with Organization")).toBeInTheDocument()
expect(screen.getByText("Share Publicly")).toBeInTheDocument()
@ -136,11 +131,9 @@ describe("TaskActions", () => {
it("sends shareCurrentTask message when organization option is selected", () => {
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
// Find button by its icon class
const buttons = screen.getAllByRole("button")
const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link"))
expect(shareButton).toBeDefined()
fireEvent.click(shareButton!)
// Find share button by its test ID and click it
const shareButton = screen.getByTestId("share-button")
fireEvent.click(shareButton)
const orgOption = screen.getByText("Share with Organization")
fireEvent.click(orgOption)
@ -154,11 +147,9 @@ describe("TaskActions", () => {
it("sends shareCurrentTask message when public option is selected", () => {
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
// Find button by its icon class
const buttons = screen.getAllByRole("button")
const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link"))
expect(shareButton).toBeDefined()
fireEvent.click(shareButton!)
// Find share button by its test ID and click it
const shareButton = screen.getByTestId("share-button")
fireEvent.click(shareButton)
const publicOption = screen.getByText("Share Publicly")
fireEvent.click(publicOption)
@ -180,11 +171,9 @@ describe("TaskActions", () => {
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
// Find button by its icon class
const buttons = screen.getAllByRole("button")
const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link"))
expect(shareButton).toBeDefined()
fireEvent.click(shareButton!)
// Find share button by its test ID and click it
const shareButton = screen.getByTestId("share-button")
fireEvent.click(shareButton)
expect(screen.queryByText("Share with Organization")).not.toBeInTheDocument()
expect(screen.getByText("Share Publicly")).toBeInTheDocument()
@ -202,11 +191,9 @@ describe("TaskActions", () => {
it("shows connect to cloud option when not authenticated", () => {
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
// Find button by its icon class
const buttons = screen.getAllByRole("button")
const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link"))
expect(shareButton).toBeDefined()
fireEvent.click(shareButton!)
// Find share button by its test ID and click it
const shareButton = screen.getByTestId("share-button")
fireEvent.click(shareButton)
expect(screen.getByText("Connect to Roo Code Cloud")).toBeInTheDocument()
expect(screen.getByText("Sign in to Roo Code Cloud to share tasks")).toBeInTheDocument()
@ -216,11 +203,9 @@ describe("TaskActions", () => {
it("does not show organization and public options when not authenticated", () => {
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
// Find button by its icon class
const buttons = screen.getAllByRole("button")
const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link"))
expect(shareButton).toBeDefined()
fireEvent.click(shareButton!)
// Find share button by its test ID and click it
const shareButton = screen.getByTestId("share-button")
fireEvent.click(shareButton)
expect(screen.queryByText("Share with Organization")).not.toBeInTheDocument()
expect(screen.queryByText("Share Publicly")).not.toBeInTheDocument()
@ -229,11 +214,9 @@ describe("TaskActions", () => {
it("sends rooCloudSignIn message when connect to cloud is selected", () => {
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
// Find button by its icon class
const buttons = screen.getAllByRole("button")
const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link"))
expect(shareButton).toBeDefined()
fireEvent.click(shareButton!)
// Find share button by its test ID and click it
const shareButton = screen.getByTestId("share-button")
fireEvent.click(shareButton)
const connectOption = screen.getByText("Connect")
fireEvent.click(connectOption)
@ -253,9 +236,8 @@ describe("TaskActions", () => {
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
// Find button by its icon class
const buttons = screen.getAllByRole("button")
const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link"))
// Find share button by its test ID
const shareButton = screen.getByTestId("share-button")
expect(shareButton).toBeInTheDocument()
expect(shareButton).toBeDisabled()
@ -303,10 +285,8 @@ describe("TaskActions", () => {
const { rerender } = render(<TaskActions item={mockItem} buttonsDisabled={false} />)
// Click share button to open connect modal
const buttons = screen.getAllByRole("button")
const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link"))
expect(shareButton).toBeDefined()
fireEvent.click(shareButton!)
const shareButton = screen.getByTestId("share-button")
fireEvent.click(shareButton)
// Click connect button to initiate authentication
const connectButton = screen.getByText("Connect")
@ -353,12 +333,11 @@ describe("TaskActions", () => {
})
})
it("renders delete button and file size when item has size", () => {
it("renders delete button when item has size", () => {
render(<TaskActions item={mockItem} buttonsDisabled={false} />)
const deleteButton = screen.getByLabelText("Delete Task (Shift + Click to skip confirmation)")
expect(deleteButton).toBeInTheDocument()
expect(screen.getByText("1024 B")).toBeInTheDocument()
})
it("does not render delete button when item has no size", () => {
@ -374,11 +353,10 @@ describe("TaskActions", () => {
it("keeps share, export, and copy buttons enabled but disables delete button when buttonsDisabled is true", () => {
render(<TaskActions item={mockItem} buttonsDisabled={true} />)
// Find buttons by their labels/icons
const buttons = screen.getAllByRole("button")
const shareButton = buttons.find((btn) => btn.querySelector(".codicon-link"))
// Find buttons by their labels/test IDs
const shareButton = screen.getByTestId("share-button")
const exportButton = screen.getByLabelText("Export task history")
const copyButton = buttons.find((btn) => btn.querySelector(".codicon-copy"))
const copyButton = screen.getByLabelText("history:copyPrompt")
const deleteButton = screen.getByLabelText("Delete Task (Shift + Click to skip confirmation)")
// Share, export, and copy buttons should be enabled regardless of buttonsDisabled
@ -393,10 +371,9 @@ describe("TaskActions", () => {
// Test with buttonsDisabled = false
const { rerender } = render(<TaskActions item={mockItem} buttonsDisabled={false} />)
let buttons = screen.getAllByRole("button")
let shareButton = buttons.find((btn) => btn.querySelector(".codicon-link"))
let shareButton = screen.getByTestId("share-button")
let exportButton = screen.getByLabelText("Export task history")
let copyButton = buttons.find((btn) => btn.querySelector(".codicon-copy"))
let copyButton = screen.getByLabelText("history:copyPrompt")
let deleteButton = screen.getByLabelText("Delete Task (Shift + Click to skip confirmation)")
expect(shareButton).not.toBeDisabled()
@ -407,10 +384,9 @@ describe("TaskActions", () => {
// Test with buttonsDisabled = true
rerender(<TaskActions item={mockItem} buttonsDisabled={true} />)
buttons = screen.getAllByRole("button")
shareButton = buttons.find((btn) => btn.querySelector(".codicon-link"))
shareButton = screen.getByTestId("share-button")
exportButton = screen.getByLabelText("Export task history")
copyButton = buttons.find((btn) => btn.querySelector(".codicon-copy"))
copyButton = screen.getByLabelText("history:copyPrompt")
deleteButton = screen.getByLabelText("Delete Task (Shift + Click to skip confirmation)")
// Share, export, and copy remain enabled

View file

@ -53,7 +53,6 @@ describe("TaskHeader", () => {
contextTokens: 200,
buttonsDisabled: false,
handleCondenseContext: vi.fn(),
onClose: vi.fn(),
}
const queryClient = new QueryClient()
@ -91,9 +90,13 @@ describe("TaskHeader", () => {
expect(screen.queryByText(/\$/)).not.toBeInTheDocument()
})
it("should render the condense context button", () => {
it("should render the condense context button when expanded", () => {
renderTaskHeader()
// Find the button that contains the FoldVertical icon
// First click to expand the task header
const taskHeader = screen.getByText("Test task")
fireEvent.click(taskHeader)
// Now find the condense button in the expanded state
const buttons = screen.getAllByRole("button")
const condenseButton = buttons.find((button) => button.querySelector("svg.lucide-fold-vertical"))
expect(condenseButton).toBeDefined()
@ -103,6 +106,11 @@ describe("TaskHeader", () => {
it("should call handleCondenseContext when condense context button is clicked", () => {
const handleCondenseContext = vi.fn()
renderTaskHeader({ handleCondenseContext })
// First click to expand the task header
const taskHeader = screen.getByText("Test task")
fireEvent.click(taskHeader)
// Find the button that contains the FoldVertical icon
const buttons = screen.getAllByRole("button")
const condenseButton = buttons.find((button) => button.querySelector("svg.lucide-fold-vertical"))
@ -114,6 +122,11 @@ describe("TaskHeader", () => {
it("should disable the condense context button when buttonsDisabled is true", () => {
const handleCondenseContext = vi.fn()
renderTaskHeader({ buttonsDisabled: true, handleCondenseContext })
// First click to expand the task header
const taskHeader = screen.getByText("Test task")
fireEvent.click(taskHeader)
// Find the button that contains the FoldVertical icon
const buttons = screen.getAllByRole("button")
const condenseButton = buttons.find((button) => button.querySelector("svg.lucide-fold-vertical"))

View file

@ -39,6 +39,7 @@ const Thumbnails = ({ images, style, setImages, onHeightChange }: ThumbnailsProp
return (
<div
ref={containerRef}
className="py-1"
style={{
display: "flex",
flexWrap: "wrap",

View file

@ -4,10 +4,11 @@
"title": "Tasca",
"seeMore": "Veure més",
"seeLess": "Veure menys",
"tokens": "Tokens:",
"cache": "Caché:",
"apiCost": "Cost d'API:",
"contextWindow": "Finestra de context:",
"tokens": "Tokens",
"cache": "Caché",
"apiCost": "Cost d'API",
"size": "Mida",
"contextWindow": "Finestra de context",
"closeAndStart": "Tancar tasca i iniciar-ne una de nova",
"export": "Exportar historial de tasques",
"delete": "Eliminar tasca (Shift + Clic per ometre confirmació)",

View file

@ -4,10 +4,11 @@
"title": "Aufgabe",
"seeMore": "Mehr anzeigen",
"seeLess": "Weniger anzeigen",
"tokens": "Tokens:",
"cache": "Cache:",
"apiCost": "API-Kosten:",
"contextWindow": "Kontextfenster:",
"tokens": "Tokens",
"cache": "Cache",
"apiCost": "API-Kosten",
"size": "Größe",
"contextWindow": "Kontextfenster",
"closeAndStart": "Aufgabe schließen und neue starten",
"export": "Aufgabenverlauf exportieren",
"delete": "Aufgabe löschen (Shift + Klick zum Überspringen der Bestätigung)",

View file

@ -4,11 +4,12 @@
"title": "Task",
"seeMore": "See more",
"seeLess": "See less",
"tokens": "Tokens:",
"cache": "Cache:",
"apiCost": "API Cost:",
"tokens": "Tokens",
"cache": "Cache",
"apiCost": "API Cost",
"size": "Size",
"condenseContext": "Intelligently condense context",
"contextWindow": "Context Length:",
"contextWindow": "Context Length",
"closeAndStart": "Close task and start a new one",
"export": "Export task history",
"share": "Share task",

View file

@ -4,10 +4,11 @@
"title": "Tarea",
"seeMore": "Ver más",
"seeLess": "Ver menos",
"tokens": "Tokens:",
"cache": "Caché:",
"apiCost": "Costo de API:",
"contextWindow": "Longitud del contexto:",
"tokens": "Tokens",
"cache": "Caché",
"apiCost": "Costo de API",
"size": "Tamaño",
"contextWindow": "Longitud del contexto",
"closeAndStart": "Cerrar tarea e iniciar una nueva",
"export": "Exportar historial de tareas",
"delete": "Eliminar tarea (Shift + Clic para omitir confirmación)",

View file

@ -4,10 +4,11 @@
"title": "Tâche",
"seeMore": "Voir plus",
"seeLess": "Voir moins",
"tokens": "Tokens :",
"cache": "Cache :",
"apiCost": "Coût API :",
"contextWindow": "Durée du contexte :",
"tokens": "Tokens",
"cache": "Cache",
"apiCost": "Coût API",
"size": "Taille",
"contextWindow": "Durée du contexte",
"closeAndStart": "Fermer la tâche et en commencer une nouvelle",
"export": "Exporter l'historique des tâches",
"delete": "Supprimer la tâche (Shift + Clic pour ignorer la confirmation)",

View file

@ -4,10 +4,11 @@
"title": "कार्य",
"seeMore": "अधिक देखें",
"seeLess": "कम देखें",
"tokens": "Tokens:",
"cache": "कैश:",
"apiCost": "API लागत:",
"contextWindow": "संदर्भ लंबाई:",
"tokens": "Tokens",
"cache": "कैश",
"apiCost": "API लागत",
"size": "आकार",
"contextWindow": "संदर्भ लंबाई",
"closeAndStart": "कार्य बंद करें और नया शुरू करें",
"export": "कार्य इतिहास निर्यात करें",
"delete": "कार्य हटाएं (पुष्टि को छोड़ने के लिए Shift + क्लिक)",

View file

@ -4,11 +4,12 @@
"title": "Tugas",
"seeMore": "Lihat lebih banyak",
"seeLess": "Lihat lebih sedikit",
"tokens": "Token:",
"cache": "Cache:",
"apiCost": "Biaya API:",
"tokens": "Token",
"cache": "Cache",
"apiCost": "Biaya API",
"size": "Ukuran",
"condenseContext": "Kondensasi konteks secara cerdas",
"contextWindow": "Panjang Konteks:",
"contextWindow": "Panjang Konteks",
"closeAndStart": "Tutup tugas dan mulai yang baru",
"export": "Ekspor riwayat tugas",
"share": "Bagikan tugas",

View file

@ -4,10 +4,11 @@
"title": "Attività",
"seeMore": "Vedi altro",
"seeLess": "Vedi meno",
"tokens": "Tokens:",
"cache": "Cache:",
"apiCost": "Costo API:",
"contextWindow": "Lunghezza del contesto:",
"tokens": "Tokens",
"cache": "Cache",
"apiCost": "Costo API",
"size": "Dimensione",
"contextWindow": "Lunghezza del contesto",
"closeAndStart": "Chiudi attività e iniziane una nuova",
"export": "Esporta cronologia attività",
"delete": "Elimina attività (Shift + Clic per saltare la conferma)",

View file

@ -4,10 +4,11 @@
"title": "タスク",
"seeMore": "もっと見る",
"seeLess": "表示を減らす",
"tokens": "トークン:",
"cache": "キャッシュ:",
"apiCost": "APIコスト:",
"contextWindow": "コンテキストウィンドウ:",
"tokens": "トークン",
"cache": "キャッシュ",
"apiCost": "APIコスト",
"size": "サイズ",
"contextWindow": "コンテキストウィンドウ",
"closeAndStart": "タスクを閉じて新しいタスクを開始",
"export": "タスク履歴をエクスポート",
"delete": "タスクを削除Shift + クリックで確認をスキップ)",

View file

@ -4,10 +4,11 @@
"title": "작업",
"seeMore": "더 보기",
"seeLess": "줄여보기",
"tokens": "토큰:",
"cache": "캐시:",
"apiCost": "API 비용:",
"contextWindow": "컨텍스트 창:",
"tokens": "토큰",
"cache": "캐시",
"apiCost": "API 비용",
"size": "크기",
"contextWindow": "컨텍스트 창",
"closeAndStart": "작업 닫고 새 작업 시작",
"export": "작업 기록 내보내기",
"delete": "작업 삭제 (Shift + 클릭으로 확인 생략)",

View file

@ -4,10 +4,11 @@
"title": "Taak",
"seeMore": "Meer weergeven",
"seeLess": "Minder weergeven",
"tokens": "Tokens:",
"cache": "Cache:",
"apiCost": "API-kosten:",
"contextWindow": "Contextlengte:",
"tokens": "Tokens",
"cache": "Cache",
"apiCost": "API-kosten",
"size": "Grootte",
"contextWindow": "Contextlengte",
"closeAndStart": "Taak sluiten en een nieuwe starten",
"export": "Taakgeschiedenis exporteren",
"delete": "Taak verwijderen (Shift + Klik om bevestiging over te slaan)",

View file

@ -4,10 +4,11 @@
"title": "Zadanie",
"seeMore": "Zobacz więcej",
"seeLess": "Zobacz mniej",
"tokens": "Tokeny:",
"cache": "Pamięć podręczna:",
"apiCost": "Koszt API:",
"contextWindow": "Okno kontekstu:",
"tokens": "Tokeny",
"cache": "Pamięć podręczna",
"apiCost": "Koszt API",
"size": "Rozmiar",
"contextWindow": "Okno kontekstu",
"closeAndStart": "Zamknij zadanie i rozpocznij nowe",
"export": "Eksportuj historię zadań",
"delete": "Usuń zadanie (Shift + Kliknięcie, aby pominąć potwierdzenie)",

View file

@ -4,10 +4,11 @@
"title": "Tarefa",
"seeMore": "Ver mais",
"seeLess": "Ver menos",
"tokens": "Tokens:",
"cache": "Cache:",
"apiCost": "Custo da API:",
"contextWindow": "Janela de contexto:",
"tokens": "Tokens",
"cache": "Cache",
"apiCost": "Custo da API",
"size": "Tamanho",
"contextWindow": "Janela de contexto",
"closeAndStart": "Fechar tarefa e iniciar nova",
"export": "Exportar histórico de tarefas",
"delete": "Excluir tarefa (Shift + Clique para pular confirmação)",

View file

@ -4,10 +4,11 @@
"title": "Задача",
"seeMore": "Показать больше",
"seeLess": "Показать меньше",
"tokens": "Токенов:",
"cache": "Кэш:",
"apiCost": "Стоимость API:",
"contextWindow": "Длина контекста:",
"tokens": "Токенов",
"cache": "Кэш",
"apiCost": "Стоимость API",
"size": "Размер",
"contextWindow": "Длина контекста",
"closeAndStart": "Закрыть задачу и начать новую",
"export": "Экспортировать историю задач",
"delete": "Удалить задачу (Shift + клик для пропуска подтверждения)",

View file

@ -4,10 +4,11 @@
"title": "Görev",
"seeMore": "Daha fazla gör",
"seeLess": "Daha az gör",
"tokens": "Tokenlar:",
"cache": "Önbellek:",
"apiCost": "API Maliyeti:",
"contextWindow": "Bağlam Uzunluğu:",
"tokens": "Tokenlar",
"cache": "Önbellek",
"apiCost": "API Maliyeti",
"size": "Boyut",
"contextWindow": "Bağlam Uzunluğu",
"closeAndStart": "Görevi kapat ve yeni bir görev başlat",
"export": "Görev geçmişini dışa aktar",
"delete": "Görevi sil (Onayı atlamak için Shift + Tıkla)",

View file

@ -4,10 +4,11 @@
"title": "Nhiệm vụ",
"seeMore": "Xem thêm",
"seeLess": "Thu gọn",
"tokens": "Tokens:",
"cache": "Bộ nhớ đệm:",
"apiCost": "Chi phí API:",
"contextWindow": "Chiều dài bối cảnh:",
"tokens": "Tokens",
"cache": "Bộ nhớ đệm",
"apiCost": "Chi phí API",
"size": "Kích thước",
"contextWindow": "Chiều dài bối cảnh",
"closeAndStart": "Đóng nhiệm vụ và bắt đầu nhiệm vụ mới",
"export": "Xuất lịch sử nhiệm vụ",
"delete": "Xóa nhiệm vụ (Shift + Click để bỏ qua xác nhận)",

View file

@ -4,10 +4,11 @@
"title": "任务",
"seeMore": "展开",
"seeLess": "收起",
"tokens": "Token 用量:",
"cache": "缓存:",
"apiCost": "API 费用:",
"contextWindow": "上下文长度:",
"tokens": "Token 用量",
"cache": "缓存",
"apiCost": "API 费用",
"size": "大小",
"contextWindow": "上下文长度",
"closeAndStart": "关闭任务并开始新任务",
"export": "导出任务历史",
"delete": "删除任务Shift + 点击跳过确认)",

View file

@ -4,9 +4,10 @@
"title": "工作",
"seeMore": "顯示更多",
"seeLess": "顯示較少",
"tokens": "Tokens:",
"cache": "快取",
"tokens": "Tokens",
"cache": "快取",
"apiCost": "API 費用:",
"size": "大小",
"contextWindow": "上下文長度:",
"closeAndStart": "關閉現有工作並開始一項新的工作",
"export": "匯出工作紀錄",