mirror of
https://github.com/pacnpal/Roo-Code.git
synced 2025-12-20 12:21:13 -05:00
Add task history
This commit is contained in:
@@ -3,7 +3,7 @@ import React from "react"
|
||||
import Markdown from "react-markdown"
|
||||
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"
|
||||
import { ClaudeAsk, ClaudeMessage, ClaudeSay, ClaudeSayTool } from "../../../src/shared/ExtensionMessage"
|
||||
import { COMMAND_OUTPUT_STRING } from "../utils/combineCommandSequences"
|
||||
import { COMMAND_OUTPUT_STRING } from "../../../src/shared/combineCommandSequences"
|
||||
import { SyntaxHighlighterStyle } from "../utils/getSyntaxHighlighterStyleFromTheme"
|
||||
import CodeBlock from "./CodeBlock/CodeBlock"
|
||||
import Thumbnails from "./Thumbnails"
|
||||
|
||||
@@ -5,25 +5,29 @@ import DynamicTextArea from "react-textarea-autosize"
|
||||
import { useEvent, useMount } from "react-use"
|
||||
import { Virtuoso, type VirtuosoHandle } from "react-virtuoso"
|
||||
import { ClaudeAsk, ClaudeMessage, ExtensionMessage } from "../../../src/shared/ExtensionMessage"
|
||||
import { combineApiRequests } from "../utils/combineApiRequests"
|
||||
import { combineCommandSequences } from "../utils/combineCommandSequences"
|
||||
import { getApiMetrics } from "../utils/getApiMetrics"
|
||||
import { getApiMetrics } from "../../../src/shared/getApiMetrics"
|
||||
import { combineApiRequests } from "../../../src/shared/combineApiRequests"
|
||||
import { combineCommandSequences } from "../../../src/shared/combineCommandSequences"
|
||||
import { getSyntaxHighlighterStyleFromTheme } from "../utils/getSyntaxHighlighterStyleFromTheme"
|
||||
import { vscode } from "../utils/vscode"
|
||||
import Announcement from "./Announcement"
|
||||
import ChatRow from "./ChatRow"
|
||||
import HistoryPreview from "./HistoryPreview"
|
||||
import TaskHeader from "./TaskHeader"
|
||||
import Thumbnails from "./Thumbnails"
|
||||
import { HistoryItem } from "../../../src/shared/HistoryItem"
|
||||
|
||||
interface ChatViewProps {
|
||||
version: string
|
||||
messages: ClaudeMessage[]
|
||||
taskHistory: HistoryItem[]
|
||||
isHidden: boolean
|
||||
vscodeThemeName?: string
|
||||
showAnnouncement: boolean
|
||||
selectedModelSupportsImages: boolean
|
||||
selectedModelSupportsPromptCache: boolean
|
||||
hideAnnouncement: () => void
|
||||
showHistoryView: () => void
|
||||
}
|
||||
|
||||
const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images
|
||||
@@ -31,14 +35,16 @@ const MAX_IMAGES_PER_MESSAGE = 20 // Anthropic limits to 20 images
|
||||
const ChatView = ({
|
||||
version,
|
||||
messages,
|
||||
taskHistory,
|
||||
isHidden,
|
||||
vscodeThemeName,
|
||||
showAnnouncement,
|
||||
selectedModelSupportsImages,
|
||||
selectedModelSupportsPromptCache,
|
||||
hideAnnouncement,
|
||||
showHistoryView,
|
||||
}: ChatViewProps) => {
|
||||
//const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined
|
||||
//const task = messages.length > 0 ? (messages[0].say === "task" ? messages[0] : undefined) : undefined) : undefined
|
||||
const task = messages.length > 0 ? messages[0] : undefined // leaving this less safe version here since if the first message is not a task, then the extension is in a bad state and needs to be debugged (see ClaudeDev.abort)
|
||||
const modifiedMessages = useMemo(() => combineApiRequests(combineCommandSequences(messages.slice(1))), [messages])
|
||||
// has to be after api_req_finished are all reduced into api_req_started messages
|
||||
@@ -137,6 +143,13 @@ const ChatView = ({
|
||||
setPrimaryButtonText("Start New Task")
|
||||
setSecondaryButtonText(undefined)
|
||||
break
|
||||
case "resume_task":
|
||||
setTextAreaDisabled(false)
|
||||
setClaudeAsk("resume_task")
|
||||
setEnableButtons(true)
|
||||
setPrimaryButtonText("Resume Task")
|
||||
setSecondaryButtonText(undefined)
|
||||
break
|
||||
}
|
||||
break
|
||||
case "say":
|
||||
@@ -199,6 +212,7 @@ const ChatView = ({
|
||||
case "command": // user can provide feedback to a tool or command use
|
||||
case "command_output": // user can send input to command stdin
|
||||
case "completion_result": // if this happens then the user has feedback for the completion result
|
||||
case "resume_task":
|
||||
vscode.postMessage({
|
||||
type: "askResponse",
|
||||
askResponse: "messageResponse",
|
||||
@@ -229,6 +243,7 @@ const ChatView = ({
|
||||
case "command":
|
||||
case "command_output":
|
||||
case "tool":
|
||||
case "resume_task":
|
||||
vscode.postMessage({ type: "askResponse", askResponse: "yesButtonTapped" })
|
||||
break
|
||||
case "completion_result":
|
||||
@@ -392,6 +407,8 @@ const ChatView = ({
|
||||
break
|
||||
case "api_req_failed": // this message is used to update the latest api_req_started that the request failed
|
||||
return false
|
||||
case "resume_task":
|
||||
return false
|
||||
}
|
||||
switch (message.say) {
|
||||
case "api_req_finished": // combineApiRequests removes this from modifiedMessages anyways
|
||||
@@ -460,7 +477,7 @@ const ChatView = ({
|
||||
) : (
|
||||
<>
|
||||
{showAnnouncement && <Announcement version={version} hideAnnouncement={hideAnnouncement} />}
|
||||
<div style={{ padding: "0 20px" }}>
|
||||
<div style={{ padding: "0 20px", flexGrow: taskHistory.length > 0 ? undefined : 1 }}>
|
||||
<h2>What can I do for you?</h2>
|
||||
<p>
|
||||
Thanks to{" "}
|
||||
@@ -474,64 +491,72 @@ const ChatView = ({
|
||||
permission), I can assist you in ways that go beyond simple code completion or tech support.
|
||||
</p>
|
||||
</div>
|
||||
{taskHistory.length > 0 && (
|
||||
<HistoryPreview taskHistory={taskHistory} showHistoryView={showHistoryView} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
className="scrollable"
|
||||
style={{
|
||||
flexGrow: 1,
|
||||
overflowY: "scroll", // always show scrollbar
|
||||
}}
|
||||
// followOutput={(isAtBottom) => {
|
||||
// const lastMessage = modifiedMessages.at(-1)
|
||||
// if (lastMessage && shouldShowChatRow(lastMessage)) {
|
||||
// return "smooth"
|
||||
// }
|
||||
// return false
|
||||
// }}
|
||||
increaseViewportBy={{ top: 0, bottom: Number.MAX_SAFE_INTEGER }} // hack to make sure the last message is always rendered to get truly perfect scroll to bottom animation when new messages are added (Number.MAX_SAFE_INTEGER is safe for arithmetic operations, which is all virtuoso uses this value for in src/sizeRangeSystem.ts)
|
||||
data={visibleMessages} // messages is the raw format returned by extension, modifiedMessages is the manipulated structure that combines certain messages of related type, and visibleMessages is the filtered structure that removes messages that should not be rendered
|
||||
itemContent={(index, message) => (
|
||||
<ChatRow
|
||||
key={message.ts}
|
||||
message={message}
|
||||
syntaxHighlighterStyle={syntaxHighlighterStyle}
|
||||
isExpanded={expandedRows[message.ts] || false}
|
||||
onToggleExpand={() => toggleRowExpansion(message.ts)}
|
||||
lastModifiedMessage={modifiedMessages.at(-1)}
|
||||
isLast={index === visibleMessages.length - 1}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
opacity: primaryButtonText || secondaryButtonText ? (enableButtons ? 1 : 0.5) : 0,
|
||||
display: "flex",
|
||||
padding: "10px 15px 0px 15px",
|
||||
}}>
|
||||
{primaryButtonText && (
|
||||
<VSCodeButton
|
||||
appearance="primary"
|
||||
disabled={!enableButtons}
|
||||
{task && (
|
||||
<>
|
||||
<Virtuoso
|
||||
ref={virtuosoRef}
|
||||
className="scrollable"
|
||||
style={{
|
||||
flex: secondaryButtonText ? 1 : 2,
|
||||
marginRight: secondaryButtonText ? "6px" : "0",
|
||||
flexGrow: 1,
|
||||
overflowY: "scroll", // always show scrollbar
|
||||
}}
|
||||
onClick={handlePrimaryButtonClick}>
|
||||
{primaryButtonText}
|
||||
</VSCodeButton>
|
||||
)}
|
||||
{secondaryButtonText && (
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
disabled={!enableButtons}
|
||||
style={{ flex: 1, marginLeft: "6px" }}
|
||||
onClick={handleSecondaryButtonClick}>
|
||||
{secondaryButtonText}
|
||||
</VSCodeButton>
|
||||
)}
|
||||
</div>
|
||||
// followOutput={(isAtBottom) => {
|
||||
// const lastMessage = modifiedMessages.at(-1)
|
||||
// if (lastMessage && shouldShowChatRow(lastMessage)) {
|
||||
// return "smooth"
|
||||
// }
|
||||
// return false
|
||||
// }}
|
||||
increaseViewportBy={{ top: 0, bottom: Number.MAX_SAFE_INTEGER }} // hack to make sure the last message is always rendered to get truly perfect scroll to bottom animation when new messages are added (Number.MAX_SAFE_INTEGER is safe for arithmetic operations, which is all virtuoso uses this value for in src/sizeRangeSystem.ts)
|
||||
data={visibleMessages} // messages is the raw format returned by extension, modifiedMessages is the manipulated structure that combines certain messages of related type, and visibleMessages is the filtered structure that removes messages that should not be rendered
|
||||
itemContent={(index, message) => (
|
||||
<ChatRow
|
||||
key={message.ts}
|
||||
message={message}
|
||||
syntaxHighlighterStyle={syntaxHighlighterStyle}
|
||||
isExpanded={expandedRows[message.ts] || false}
|
||||
onToggleExpand={() => toggleRowExpansion(message.ts)}
|
||||
lastModifiedMessage={modifiedMessages.at(-1)}
|
||||
isLast={index === visibleMessages.length - 1}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
opacity: primaryButtonText || secondaryButtonText ? (enableButtons ? 1 : 0.5) : 0,
|
||||
display: "flex",
|
||||
padding: "10px 15px 0px 15px",
|
||||
}}>
|
||||
{primaryButtonText && (
|
||||
<VSCodeButton
|
||||
appearance="primary"
|
||||
disabled={!enableButtons}
|
||||
style={{
|
||||
flex: secondaryButtonText ? 1 : 2,
|
||||
marginRight: secondaryButtonText ? "6px" : "0",
|
||||
}}
|
||||
onClick={handlePrimaryButtonClick}>
|
||||
{primaryButtonText}
|
||||
</VSCodeButton>
|
||||
)}
|
||||
{secondaryButtonText && (
|
||||
<VSCodeButton
|
||||
appearance="secondary"
|
||||
disabled={!enableButtons}
|
||||
style={{ flex: 1, marginLeft: "6px" }}
|
||||
onClick={handleSecondaryButtonClick}>
|
||||
{secondaryButtonText}
|
||||
</VSCodeButton>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div
|
||||
style={{
|
||||
padding: "10px 15px",
|
||||
|
||||
141
webview-ui/src/components/HistoryPreview.tsx
Normal file
141
webview-ui/src/components/HistoryPreview.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import { vscode } from "../utils/vscode"
|
||||
import { HistoryItem } from "../../../src/shared/HistoryItem"
|
||||
|
||||
type HistoryPreviewProps = {
|
||||
taskHistory: HistoryItem[]
|
||||
showHistoryView: () => void
|
||||
}
|
||||
|
||||
const HistoryPreview = ({ taskHistory, showHistoryView }: HistoryPreviewProps) => {
|
||||
const handleHistorySelect = (id: string) => {
|
||||
vscode.postMessage({ type: "showTaskWithId", text: id })
|
||||
}
|
||||
|
||||
const formatDate = (timestamp: number) => {
|
||||
const date = new Date(timestamp)
|
||||
return date
|
||||
.toLocaleString("en-US", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
})
|
||||
.replace(", ", " ")
|
||||
.replace(" at", ",")
|
||||
.toUpperCase()
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ flexGrow: 1, overflowY: "auto" }}>
|
||||
<style>
|
||||
{`
|
||||
.history-preview-item {
|
||||
background-color: color-mix(in srgb, var(--vscode-toolbar-hoverBackground) 50%, transparent);
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
opacity: 0.8;
|
||||
cursor: pointer;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.history-preview-item:hover {
|
||||
background-color: color-mix(in srgb, var(--vscode-toolbar-hoverBackground) 100%, transparent);
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
|
||||
<div
|
||||
style={{
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
margin: "10px 20px 10px 20px",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
}}>
|
||||
<span
|
||||
className="codicon codicon-comment-discussion"
|
||||
style={{ marginRight: "4px", transform: "scale(0.9)" }}></span>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
fontSize: "0.85em",
|
||||
textTransform: "uppercase",
|
||||
}}>
|
||||
Recent Tasks
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style={{ padding: "0px 20px 0 20px" }}>
|
||||
{taskHistory.slice(0, 3).map((item) => (
|
||||
<div key={item.id} className="history-preview-item" onClick={() => handleHistorySelect(item.id)}>
|
||||
<div style={{ padding: "12px" }}>
|
||||
<div style={{ marginBottom: "8px" }}>
|
||||
<span
|
||||
style={{
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
fontWeight: 500,
|
||||
fontSize: "0.85em",
|
||||
textTransform: "uppercase",
|
||||
}}>
|
||||
{formatDate(item.ts)}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "var(--vscode-font-size)",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
marginBottom: "8px",
|
||||
display: "-webkit-box",
|
||||
WebkitLineClamp: 3,
|
||||
WebkitBoxOrient: "vertical",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word",
|
||||
overflowWrap: "anywhere",
|
||||
}}>
|
||||
{item.task}
|
||||
</div>
|
||||
<div style={{ fontSize: "0.85em", color: "var(--vscode-descriptionForeground)" }}>
|
||||
<span>
|
||||
Tokens: ↑{item.tokensIn.toLocaleString()} ↓{item.tokensOut.toLocaleString()}
|
||||
</span>
|
||||
{" • "}
|
||||
{item.cacheWrites && item.cacheReads && (
|
||||
<>
|
||||
<span>
|
||||
Cache: +{item.cacheWrites.toLocaleString()} →{" "}
|
||||
{item.cacheReads.toLocaleString()}
|
||||
</span>
|
||||
{" • "}
|
||||
</>
|
||||
)}
|
||||
<span>API Cost: ${item.totalCost.toFixed(4)}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "center" }}>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={() => showHistoryView()}
|
||||
style={{
|
||||
opacity: 0.9,
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "var(--vscode-font-size)",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
View all history
|
||||
</div>
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default HistoryPreview
|
||||
295
webview-ui/src/components/HistoryView.tsx
Normal file
295
webview-ui/src/components/HistoryView.tsx
Normal file
@@ -0,0 +1,295 @@
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import { vscode } from "../utils/vscode"
|
||||
import { HistoryItem } from "../../../src/shared/HistoryItem"
|
||||
|
||||
type HistoryViewProps = {
|
||||
taskHistory: HistoryItem[]
|
||||
onDone: () => void
|
||||
}
|
||||
|
||||
const HistoryView = ({ taskHistory, onDone }: HistoryViewProps) => {
|
||||
const handleHistorySelect = (id: string) => {
|
||||
vscode.postMessage({ type: "showTaskWithId", text: id })
|
||||
}
|
||||
|
||||
const handleDeleteHistoryItem = (id: string) => {
|
||||
vscode.postMessage({ type: "deleteTaskWithId", text: id })
|
||||
}
|
||||
|
||||
const handleExportMd = (id: string) => {
|
||||
vscode.postMessage({ type: "exportTaskWithId", text: id })
|
||||
}
|
||||
|
||||
const formatDate = (timestamp: number) => {
|
||||
const date = new Date(timestamp)
|
||||
return date
|
||||
.toLocaleString("en-US", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
hour12: true,
|
||||
})
|
||||
.replace(", ", " ")
|
||||
.replace(" at", ",")
|
||||
.toUpperCase()
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<style>
|
||||
{`
|
||||
.history-item:hover {
|
||||
background-color: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
.delete-button {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
.history-item:hover .delete-button {
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
overflow: "hidden",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
padding: "10px 16px 10px 20px",
|
||||
}}>
|
||||
<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>History</h3>
|
||||
<VSCodeButton onClick={onDone}>Done</VSCodeButton>
|
||||
</div>
|
||||
<div style={{ flexGrow: 1, overflowY: "auto", margin: 0 }}>
|
||||
{taskHistory.length === 0 && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
height: "100%",
|
||||
fontStyle: "italic",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
textAlign: "center",
|
||||
padding: "0px 10px",
|
||||
}}>
|
||||
<span
|
||||
className="codicon codicon-archive"
|
||||
style={{ fontSize: "50px", marginBottom: "15px" }}></span>
|
||||
<div>
|
||||
No history found,
|
||||
<br />
|
||||
start a new task to see it here...
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{taskHistory.map((item, index) => (
|
||||
<div
|
||||
key={item.id}
|
||||
className="history-item"
|
||||
style={{
|
||||
cursor: "pointer",
|
||||
borderBottom:
|
||||
index < taskHistory.length - 1 ? "1px solid var(--vscode-panel-border)" : "none",
|
||||
}}
|
||||
onClick={() => handleHistorySelect(item.id)}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
gap: "8px",
|
||||
padding: "12px 20px",
|
||||
position: "relative",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
}}>
|
||||
<span
|
||||
style={{
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
fontWeight: 500,
|
||||
fontSize: "0.85em",
|
||||
textTransform: "uppercase",
|
||||
}}>
|
||||
{formatDate(item.ts)}
|
||||
</span>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleDeleteHistoryItem(item.id)
|
||||
}}
|
||||
className="delete-button">
|
||||
<span className="codicon codicon-trash"></span>
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
fontSize: "var(--vscode-font-size)",
|
||||
color: "var(--vscode-foreground)",
|
||||
display: "-webkit-box",
|
||||
WebkitLineClamp: 3,
|
||||
WebkitBoxOrient: "vertical",
|
||||
overflow: "hidden",
|
||||
whiteSpace: "pre-wrap",
|
||||
wordBreak: "break-word",
|
||||
overflowWrap: "anywhere",
|
||||
}}>
|
||||
{item.task}
|
||||
</div>
|
||||
<div style={{ display: "flex", flexDirection: "column", gap: "4px" }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "4px",
|
||||
flexWrap: "wrap",
|
||||
}}>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
Tokens:
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "3px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
<i
|
||||
className="codicon codicon-arrow-up"
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
fontWeight: "bold",
|
||||
marginBottom: "-2px",
|
||||
}}
|
||||
/>
|
||||
{item.tokensIn.toLocaleString()}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "3px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
<i
|
||||
className="codicon codicon-arrow-down"
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
fontWeight: "bold",
|
||||
marginBottom: "-2px",
|
||||
}}
|
||||
/>
|
||||
{item.tokensOut.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
{item.cacheWrites && item.cacheReads && (
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "4px",
|
||||
flexWrap: "wrap",
|
||||
}}>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
Cache:
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "3px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
<i
|
||||
className="codicon codicon-database"
|
||||
style={{
|
||||
fontSize: "12px",
|
||||
fontWeight: "bold",
|
||||
marginBottom: "-1px",
|
||||
}}
|
||||
/>
|
||||
+{item.cacheWrites.toLocaleString()}
|
||||
</span>
|
||||
<span
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: "3px",
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
<i
|
||||
className="codicon codicon-arrow-right"
|
||||
style={{ fontSize: "12px", fontWeight: "bold", marginBottom: 0 }}
|
||||
/>
|
||||
{item.cacheReads.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
marginTop: -2,
|
||||
}}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "4px" }}>
|
||||
<span
|
||||
style={{
|
||||
fontWeight: 500,
|
||||
color: "var(--vscode-descriptionForeground)",
|
||||
}}>
|
||||
API Cost:
|
||||
</span>
|
||||
<span style={{ color: "var(--vscode-descriptionForeground)" }}>
|
||||
${item.totalCost.toFixed(4)}
|
||||
</span>
|
||||
</div>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation()
|
||||
handleExportMd(item.id)
|
||||
}}>
|
||||
<div style={{ fontSize: "11px", fontWeight: 500, opacity: 1 }}>
|
||||
EXPORT .MD
|
||||
</div>
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default HistoryView
|
||||
167
webview-ui/src/components/TabNavbar.tsx
Normal file
167
webview-ui/src/components/TabNavbar.tsx
Normal file
@@ -0,0 +1,167 @@
|
||||
import { VSCodeButton } from "@vscode/webview-ui-toolkit/react"
|
||||
import React, { useState } from "react"
|
||||
|
||||
export const TAB_NAVBAR_HEIGHT = 24
|
||||
const BUTTON_MARGIN_RIGHT = "3px"
|
||||
const LAST_BUTTON_MARGIN_RIGHT = "13px"
|
||||
|
||||
type TabNavbarProps = {
|
||||
onPlusClick: () => void
|
||||
onHistoryClick: () => void
|
||||
onSettingsClick: () => void
|
||||
}
|
||||
|
||||
type TooltipProps = {
|
||||
text: string
|
||||
isVisible: boolean
|
||||
position: { x: number; y: number }
|
||||
align?: "left" | "center" | "right"
|
||||
}
|
||||
|
||||
const Tooltip: React.FC<TooltipProps> = ({ text, isVisible, position, align = "center" }) => {
|
||||
let leftPosition = position.x
|
||||
let triangleStyle: React.CSSProperties = {
|
||||
left: "50%",
|
||||
marginLeft: "-5px",
|
||||
}
|
||||
|
||||
if (align === "right") {
|
||||
leftPosition = position.x - 10 // Adjust this value as needed
|
||||
triangleStyle = {
|
||||
right: "10px", // Adjust this value to match the tooltip's right padding
|
||||
marginLeft: "0",
|
||||
}
|
||||
} else if (align === "left") {
|
||||
leftPosition = position.x + 10 // Adjust this value as needed
|
||||
triangleStyle = {
|
||||
left: "10px", // Adjust this value to match the tooltip's left padding
|
||||
marginLeft: "0",
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: "fixed",
|
||||
top: `${position.y}px`,
|
||||
left: align === "center" ? leftPosition + "px" : "auto",
|
||||
right: align === "right" ? "10px" : "auto", // Ensure 10px from screen edge
|
||||
transform: align === "center" ? "translateX(-50%)" : "none",
|
||||
opacity: isVisible ? 1 : 0,
|
||||
visibility: isVisible ? "visible" : "hidden",
|
||||
transition: "opacity 0.1s ease-out 0.1s, visibility 0.1s ease-out 0.1s",
|
||||
backgroundColor: "var(--vscode-editorHoverWidget-background)",
|
||||
color: "var(--vscode-editorHoverWidget-foreground)",
|
||||
padding: "4px 8px",
|
||||
borderRadius: "3px",
|
||||
fontSize: "12px",
|
||||
pointerEvents: "none",
|
||||
zIndex: 1000,
|
||||
boxShadow: "0 2px 8px var(--vscode-widget-shadow)",
|
||||
border: "1px solid var(--vscode-editorHoverWidget-border)",
|
||||
textAlign: "center",
|
||||
whiteSpace: "nowrap",
|
||||
}}>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "-5px",
|
||||
...triangleStyle,
|
||||
borderLeft: "5px solid transparent",
|
||||
borderRight: "5px solid transparent",
|
||||
borderBottom: "5px solid var(--vscode-editorHoverWidget-border)",
|
||||
}}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: "-4px",
|
||||
...triangleStyle,
|
||||
borderLeft: "5px solid transparent",
|
||||
borderRight: "5px solid transparent",
|
||||
borderBottom: "5px solid var(--vscode-editorHoverWidget-background)",
|
||||
}}
|
||||
/>
|
||||
{text}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const TabNavbar = ({ onPlusClick, onHistoryClick, onSettingsClick }: TabNavbarProps) => {
|
||||
const [tooltip, setTooltip] = useState<TooltipProps>({
|
||||
text: "",
|
||||
isVisible: false,
|
||||
position: { x: 0, y: 0 },
|
||||
align: "center",
|
||||
})
|
||||
|
||||
const showTooltip = (text: string, event: React.MouseEvent, align: "left" | "center" | "right" = "center") => {
|
||||
const rect = event.currentTarget.getBoundingClientRect()
|
||||
setTooltip({
|
||||
text,
|
||||
isVisible: true,
|
||||
position: { x: rect.left + rect.width / 2, y: rect.bottom + 7 },
|
||||
align,
|
||||
})
|
||||
}
|
||||
|
||||
const hideTooltip = () => {
|
||||
setTooltip((prev) => ({ ...prev, isVisible: false }))
|
||||
}
|
||||
|
||||
const buttonStyle = {
|
||||
marginRight: BUTTON_MARGIN_RIGHT,
|
||||
}
|
||||
|
||||
const lastButtonStyle = {
|
||||
...buttonStyle,
|
||||
marginRight: LAST_BUTTON_MARGIN_RIGHT,
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 4,
|
||||
right: 0,
|
||||
left: 0,
|
||||
height: TAB_NAVBAR_HEIGHT,
|
||||
display: "flex",
|
||||
justifyContent: "flex-end",
|
||||
alignItems: "center",
|
||||
}}>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={onPlusClick}
|
||||
style={buttonStyle}
|
||||
onMouseEnter={(e) => showTooltip("New Chat", e, "center")}
|
||||
onMouseLeave={hideTooltip}
|
||||
onMouseMove={(e) => showTooltip("New Chat", e, "center")}>
|
||||
<span className="codicon codicon-add"></span>
|
||||
</VSCodeButton>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={onHistoryClick}
|
||||
style={buttonStyle}
|
||||
onMouseEnter={(e) => showTooltip("History", e, "center")}
|
||||
onMouseLeave={hideTooltip}
|
||||
onMouseMove={(e) => showTooltip("History", e, "center")}>
|
||||
<span className="codicon codicon-history"></span>
|
||||
</VSCodeButton>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={onSettingsClick}
|
||||
style={lastButtonStyle}
|
||||
onMouseEnter={(e) => showTooltip("Settings", e, "right")}
|
||||
onMouseLeave={hideTooltip}
|
||||
onMouseMove={(e) => showTooltip("Settings", e, "right")}>
|
||||
<span className="codicon codicon-settings-gear"></span>
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
<Tooltip {...tooltip} />
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default TabNavbar
|
||||
@@ -92,11 +92,11 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
||||
const toggleExpand = () => setIsExpanded(!isExpanded)
|
||||
|
||||
const handleDownload = () => {
|
||||
vscode.postMessage({ type: "downloadTask" })
|
||||
vscode.postMessage({ type: "exportCurrentTask" })
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={{ padding: "15px 15px 10px 15px" }}>
|
||||
<div style={{ padding: "10px 13px 10px 13px" }}>
|
||||
<div
|
||||
style={{
|
||||
backgroundColor: "var(--vscode-badge-background)",
|
||||
@@ -118,7 +118,7 @@ const TaskHeader: React.FC<TaskHeaderProps> = ({
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={onClose}
|
||||
style={{ marginTop: "-5px", marginRight: "-5px" }}>
|
||||
style={{ marginTop: "-6px", marginRight: "-4px" }}>
|
||||
<span className="codicon codicon-close"></span>
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user