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

- Created new TaskActivityBar component to display task details at the top
- Simplified TaskHeader to only show context window progress and todo list
- Updated ChatView to integrate both components properly
- Maintains all existing functionality while improving UI layout
- Addresses UX concerns about task details taking up UI space

Fixes #5652
This commit is contained in:
Roo Code 2025-07-13 06:49:58 +00:00
parent e84dd0a2cf
commit 4761f8aec6
4 changed files with 95 additions and 201 deletions

View file

@ -48,6 +48,7 @@ import BrowserSessionRow from "./BrowserSessionRow"
import ChatRow from "./ChatRow" import ChatRow from "./ChatRow"
import ChatTextArea from "./ChatTextArea" import ChatTextArea from "./ChatTextArea"
import TaskHeader from "./TaskHeader" import TaskHeader from "./TaskHeader"
import { TaskActivityBar } from "./TaskActivityBar"
import AutoApproveMenu from "./AutoApproveMenu" import AutoApproveMenu from "./AutoApproveMenu"
import SystemPromptWarning from "./SystemPromptWarning" import SystemPromptWarning from "./SystemPromptWarning"
import ProfileViolationWarning from "./ProfileViolationWarning" import ProfileViolationWarning from "./ProfileViolationWarning"
@ -1612,17 +1613,20 @@ const ChatViewComponent: React.ForwardRefRenderFunction<ChatViewRef, ChatViewPro
)} )}
{task ? ( {task ? (
<> <>
<TaskHeader <TaskActivityBar
task={task} task={task}
tokensIn={apiMetrics.totalTokensIn} tokensIn={apiMetrics.totalTokensIn}
tokensOut={apiMetrics.totalTokensOut} tokensOut={apiMetrics.totalTokensOut}
cacheWrites={apiMetrics.totalCacheWrites} cacheWrites={apiMetrics.totalCacheWrites || 0}
cacheReads={apiMetrics.totalCacheReads} cacheReads={apiMetrics.totalCacheReads || 0}
totalCost={apiMetrics.totalCost} totalCost={apiMetrics.totalCost}
onClose={handleTaskCloseButtonClick}
/>
<TaskHeader
task={task}
contextTokens={apiMetrics.contextTokens} contextTokens={apiMetrics.contextTokens}
buttonsDisabled={sendingDisabled} buttonsDisabled={sendingDisabled}
handleCondenseContext={handleCondenseContext} handleCondenseContext={handleCondenseContext}
onClose={handleTaskCloseButtonClick}
todos={latestTodos} todos={latestTodos}
/> />

View file

@ -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>
)
}

View file

@ -1,65 +1,33 @@
import { memo, useRef, useState } from "react" import { memo } from "react"
import { useWindowSize } from "react-use"
import { useTranslation } from "react-i18next" import { useTranslation } from "react-i18next"
import { VSCodeBadge } from "@vscode/webview-ui-toolkit/react" import { FoldVertical } from "lucide-react"
import { CloudUpload, CloudDownload, FoldVertical } from "lucide-react"
import type { ClineMessage } from "@roo-code/types" import type { ClineMessage } from "@roo-code/types"
import { getModelMaxOutputTokens } from "@roo/api" import { getModelMaxOutputTokens } from "@roo/api"
import { formatLargeNumber } from "@src/utils/format" import { StandardTooltip } from "@src/components/ui"
import { cn } from "@src/lib/utils"
import { Button, StandardTooltip } from "@src/components/ui"
import { useExtensionState } from "@src/context/ExtensionStateContext" import { useExtensionState } from "@src/context/ExtensionStateContext"
import { useSelectedModel } from "@/components/ui/hooks/useSelectedModel" 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 { ContextWindowProgress } from "./ContextWindowProgress"
import { Mention } from "./Mention"
import { TodoListDisplay } from "./TodoListDisplay" import { TodoListDisplay } from "./TodoListDisplay"
export interface TaskHeaderProps { export interface TaskHeaderProps {
task: ClineMessage task: ClineMessage
tokensIn: number
tokensOut: number
cacheWrites?: number
cacheReads?: number
totalCost: number
contextTokens: number contextTokens: number
buttonsDisabled: boolean buttonsDisabled: boolean
handleCondenseContext: (taskId: string) => void handleCondenseContext: (taskId: string) => void
onClose: () => void
todos?: any[] todos?: any[]
} }
const TaskHeader = ({ const TaskHeader = ({ task, contextTokens, buttonsDisabled, handleCondenseContext, todos }: TaskHeaderProps) => {
task,
tokensIn,
tokensOut,
cacheWrites,
cacheReads,
totalCost,
contextTokens,
buttonsDisabled,
handleCondenseContext,
onClose,
todos,
}: TaskHeaderProps) => {
const { t } = useTranslation() const { t } = useTranslation()
const { apiConfiguration, currentTaskItem } = useExtensionState() const { apiConfiguration, currentTaskItem } = useExtensionState()
const { id: modelId, info: model } = useSelectedModel(apiConfiguration) 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 contextWindow = model?.contextWindow || 1
const { width: windowWidth } = useWindowSize()
const condenseButton = ( const condenseButton = (
<StandardTooltip content={t("chat:task.condenseContext")}> <StandardTooltip content={t("chat:task.condenseContext")}>
<button <button
@ -75,42 +43,15 @@ const TaskHeader = ({
return ( return (
<div className="py-2 px-3"> <div className="py-2 px-3">
<div {/* Context Window Progress */}
className={cn( {contextWindow > 0 && (
"p-2.5 flex flex-col gap-1.5 relative z-1 border", <div className="p-2.5 flex flex-col gap-1.5 relative z-1 border border-vscode-panel-border/80 rounded-xs mb-2">
hasTodos ? "rounded-t-xs border-b-0" : "rounded-xs", <div className="flex items-center gap-1 flex-shrink-0">
isTaskExpanded <span className="font-bold text-sm text-vscode-foreground/80">
? "border-vscode-panel-border text-vscode-foreground" {t("chat:task.contextWindow")}
: "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> </span>
{!isTaskExpanded && (
<span className="ml-1">
<Mention text={task.text} />
</span>
)}
</div> </div>
</div> <div className="w-full flex flex-row items-center gap-1 h-auto">
<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 <ContextWindowProgress
contextWindow={contextWindow} contextWindow={contextWindow}
contextTokens={contextTokens || 0} contextTokens={contextTokens || 0}
@ -121,105 +62,11 @@ const TaskHeader = ({
} }
/> />
{condenseButton} {condenseButton}
<ShareButton item={currentTaskItem} disabled={buttonsDisabled} />
{!!totalCost && <VSCodeBadge>${totalCost.toFixed(2)}</VSCodeBadge>}
</div> </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 && ( {/* Todo List */}
<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>
<TodoListDisplay todos={todos ?? (task as any)?.tool?.todos ?? []} /> <TodoListDisplay todos={todos ?? (task as any)?.tool?.todos ?? []} />
</div> </div>
) )

View file

@ -47,13 +47,10 @@ vi.mock("@src/context/ExtensionStateContext", () => ({
describe("TaskHeader", () => { describe("TaskHeader", () => {
const defaultProps: TaskHeaderProps = { const defaultProps: TaskHeaderProps = {
task: { type: "say", ts: Date.now(), text: "Test task", images: [] }, task: { type: "say", ts: Date.now(), text: "Test task", images: [] },
tokensIn: 100,
tokensOut: 50,
totalCost: 0.05,
contextTokens: 200, contextTokens: 200,
buttonsDisabled: false, buttonsDisabled: false,
handleCondenseContext: vi.fn(), handleCondenseContext: vi.fn(),
onClose: vi.fn(), todos: [],
} }
const queryClient = new QueryClient() const queryClient = new QueryClient()
@ -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", () => { it("should render the condense context button", () => {
renderTaskHeader() renderTaskHeader()
// Find the button that contains the FoldVertical icon // Find the button that contains the FoldVertical icon