Skip to content

feat: move task details to top activity bar (#5652) #5653

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions webview-ui/src/components/chat/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ import BrowserSessionRow from "./BrowserSessionRow"
import ChatRow from "./ChatRow"
import ChatTextArea from "./ChatTextArea"
import TaskHeader from "./TaskHeader"
import { TaskActivityBar } from "./TaskActivityBar"
import AutoApproveMenu from "./AutoApproveMenu"
import SystemPromptWarning from "./SystemPromptWarning"
import ProfileViolationWarning from "./ProfileViolationWarning"
Expand Down Expand Up @@ -1612,17 +1613,20 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
)}
{task ? (
<>
<TaskHeader
<TaskActivityBar
task={task}
tokensIn={apiMetrics.totalTokensIn}
tokensOut={apiMetrics.totalTokensOut}
cacheWrites={apiMetrics.totalCacheWrites}
cacheReads={apiMetrics.totalCacheReads}
cacheWrites={apiMetrics.totalCacheWrites || 0}
cacheReads={apiMetrics.totalCacheReads || 0}
totalCost={apiMetrics.totalCost}
onClose={handleTaskCloseButtonClick}
/>
<TaskHeader
task={task}
contextTokens={apiMetrics.contextTokens}
buttonsDisabled={sendingDisabled}
handleCondenseContext={handleCondenseContext}
onClose={handleTaskCloseButtonClick}
todos={latestTodos}
/>

Expand Down
71 changes: 71 additions & 0 deletions webview-ui/src/components/chat/TaskActivityBar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import React from "react"
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
import { formatLargeNumber } from "@src/utils/format"
import type { ClineMessage } from "@roo-code/types"

interface TaskActivityBarProps {
task: ClineMessage
tokensIn: number
tokensOut: number
cacheWrites: number
cacheReads: number
totalCost: number
onClose: () => void
}

export const TaskActivityBar: React.FC<TaskActivityBarProps> = ({
task,
tokensIn,
tokensOut,
cacheWrites,
cacheReads,
totalCost,
onClose,
}) => {
if (!task) {
return null
}

const totalTokens = tokensIn + tokensOut + cacheWrites + cacheReads
const taskText = task.text || ""

return (
<div className="bg-vscode-editor-background border-b border-vscode-panel-border p-3">
<div className="flex items-center justify-between gap-4">
{/* Task Text */}
<div className="flex-1 min-w-0">
<div className="text-sm text-vscode-foreground font-medium truncate" title={taskText}>
{taskText}
</div>
</div>

{/* Metrics */}
<div className="flex items-center gap-4 text-xs text-vscode-descriptionForeground">
{/* Tokens */}
{totalTokens > 0 && (
<div className="flex items-center gap-2">
<span>Tokens: {formatLargeNumber(totalTokens)}</span>
{(cacheWrites > 0 || cacheReads > 0) && (
<span className="text-vscode-charts-blue">
(Cache: {formatLargeNumber(cacheWrites + cacheReads)})
</span>
)}
</div>
)}

{/* Cost */}
{totalCost !== undefined && totalCost > 0 && (
<div className="text-vscode-charts-green">Cost: ${totalCost.toFixed(2)}</div>
)}
</div>

{/* Actions */}
<div className="flex items-center gap-2">
<VSCodeButton appearance="icon" onClick={onClose} title="Close Task" className="!p-1">
<span className="codicon codicon-close"></span>
</VSCodeButton>
</div>
</div>
</div>
)
}
185 changes: 15 additions & 170 deletions webview-ui/src/components/chat/TaskHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,65 +1,33 @@
import { memo, useRef, useState } from "react"
import { useWindowSize } from "react-use"
import { memo } from "react"
import { useTranslation } from "react-i18next"
import { VSCodeBadge } from "@vscode/webview-ui-toolkit/react"
import { CloudUpload, CloudDownload, FoldVertical } from "lucide-react"
import { FoldVertical } from "lucide-react"

import type { ClineMessage } from "@roo-code/types"

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"

export interface TaskHeaderProps {
task: ClineMessage
tokensIn: number
tokensOut: number
cacheWrites?: number
cacheReads?: number
totalCost: number
contextTokens: number
buttonsDisabled: boolean
handleCondenseContext: (taskId: string) => void
onClose: () => void
todos?: any[]
}

const TaskHeader = ({
task,
tokensIn,
tokensOut,
cacheWrites,
cacheReads,
totalCost,
contextTokens,
buttonsDisabled,
handleCondenseContext,
onClose,
todos,
}: TaskHeaderProps) => {
const TaskHeader = ({ task, contextTokens, buttonsDisabled, handleCondenseContext, todos }: TaskHeaderProps) => {
const { t } = useTranslation()
const { apiConfiguration, currentTaskItem } = useExtensionState()
const { id: modelId, info: model } = useSelectedModel(apiConfiguration)
const [isTaskExpanded, setIsTaskExpanded] = useState(false)

const textContainerRef = useRef<HTMLDivElement>(null)
const textRef = useRef<HTMLDivElement>(null)
const contextWindow = model?.contextWindow || 1

const { width: windowWidth } = useWindowSize()

const condenseButton = (
<StandardTooltip content={t("chat:task.condenseContext")}>
<button
Expand All @@ -71,46 +39,17 @@ const TaskHeader = ({
</StandardTooltip>
)

const hasTodos = todos && Array.isArray(todos) && todos.length > 0

return (
<div className="py-2 px-3">
<div
className={cn(
"p-2.5 flex flex-col gap-1.5 relative z-1 border",
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>
{!isTaskExpanded && (
<span className="ml-1">
<Mention text={task.text} />
</span>
)}
</div>
{/* Context Window Progress */}
{contextWindow > 0 && (
<div className="p-2.5 flex flex-col gap-1.5 relative z-1 border border-vscode-panel-border/80 rounded-xs mb-2">
<div className="flex items-center gap-1 flex-shrink-0">
<span className="font-bold text-sm text-vscode-foreground/80">
{t("chat:task.contextWindow")}
</span>
</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`}>
<div className="w-full flex flex-row items-center gap-1 h-auto">
<ContextWindowProgress
contextWindow={contextWindow}
contextTokens={contextTokens || 0}
Expand All @@ -121,105 +60,11 @@ const TaskHeader = ({
}
/>
{condenseButton}
<ShareButton item={currentTaskItem} disabled={buttonsDisabled} />
{!!totalCost && <VSCodeBadge>${totalCost.toFixed(2)}</VSCodeBadge>}
</div>
)}
{/* Expanded state: Show task text and images */}
{isTaskExpanded && (
<>
<div
ref={textContainerRef}
className="-mt-0.5 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"
style={{
display: "-webkit-box",
WebkitLineClamp: "unset",
WebkitBoxOrient: "vertical",
}}>
<Mention text={task.text} />
</div>
</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>
)}
{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>
)}
</div>
)}

{!!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>
)}
</div>
</>
)}
</div>
{/* Todo List */}
<TodoListDisplay todos={todos ?? (task as any)?.tool?.todos ?? []} />
</div>
)
Expand Down
30 changes: 1 addition & 29 deletions webview-ui/src/components/chat/__tests__/TaskHeader.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,10 @@ vi.mock("@src/context/ExtensionStateContext", () => ({
describe("TaskHeader", () => {
const defaultProps: TaskHeaderProps = {
task: { type: "say", ts: Date.now(), text: "Test task", images: [] },
tokensIn: 100,
tokensOut: 50,
totalCost: 0.05,
contextTokens: 200,
buttonsDisabled: false,
handleCondenseContext: vi.fn(),
onClose: vi.fn(),
todos: [],
}

const queryClient = new QueryClient()
Expand All @@ -66,31 +63,6 @@ describe("TaskHeader", () => {
)
}

it("should display cost when totalCost is greater than 0", () => {
renderTaskHeader()
expect(screen.getByText("$0.05")).toBeInTheDocument()
})

it("should not display cost when totalCost is 0", () => {
renderTaskHeader({ totalCost: 0 })
expect(screen.queryByText("$0.0000")).not.toBeInTheDocument()
})

it("should not display cost when totalCost is null", () => {
renderTaskHeader({ totalCost: null as any })
expect(screen.queryByText(/\$/)).not.toBeInTheDocument()
})

it("should not display cost when totalCost is undefined", () => {
renderTaskHeader({ totalCost: undefined as any })
expect(screen.queryByText(/\$/)).not.toBeInTheDocument()
})

it("should not display cost when totalCost is NaN", () => {
renderTaskHeader({ totalCost: NaN })
expect(screen.queryByText(/\$/)).not.toBeInTheDocument()
})

it("should render the condense context button", () => {
renderTaskHeader()
// Find the button that contains the FoldVertical icon
Expand Down