mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-09 03:17:58 +00:00
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:
parent
e84dd0a2cf
commit
4761f8aec6
4 changed files with 95 additions and 201 deletions
|
|
@ -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}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
|
||||||
71
webview-ui/src/components/chat/TaskActivityBar.tsx
Normal file
71
webview-ui/src/components/chat/TaskActivityBar.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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",
|
</span>
|
||||||
)}>
|
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
<StandardTooltip content={t("chat:task.closeAndStart")}>
|
<div className="w-full flex flex-row items-center gap-1 h-auto">
|
||||||
<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>
|
||||||
)}
|
</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">
|
{/* Todo List */}
|
||||||
{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>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{!!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>
|
|
||||||
<TodoListDisplay todos={todos ?? (task as any)?.tool?.todos ?? []} />
|
<TodoListDisplay todos={todos ?? (task as any)?.tool?.todos ?? []} />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue