Skip to content

Commit 6762448

Browse files
committed
Clean up the task header a bit
1 parent ea1de7b commit 6762448

File tree

7 files changed

+245
-203
lines changed

7 files changed

+245
-203
lines changed

src/api/providers/openrouter.ts

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import OpenAI from "openai"
66
import { ApiHandlerOptions, ModelInfo, openRouterDefaultModelId, openRouterDefaultModelInfo } from "../../shared/api"
77
import { parseApiPrice } from "../../utils/cost"
88
import { convertToOpenAiMessages } from "../transform/openai-format"
9-
import { ApiStreamChunk, ApiStreamUsageChunk } from "../transform/stream"
9+
import { ApiStreamChunk } from "../transform/stream"
1010
import { convertToR1Format } from "../transform/r1-format"
1111

1212
import { DEFAULT_HEADERS, DEEP_SEEK_DEFAULT_TEMPERATURE } from "./constants"
@@ -30,21 +30,18 @@ type OpenRouterChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParams & {
3030

3131
// See `OpenAI.Chat.Completions.ChatCompletionChunk["usage"]`
3232
// `CompletionsAPI.CompletionUsage`
33+
// See also: https://openrouter.ai/docs/use-cases/usage-accounting
3334
interface CompletionUsage {
3435
completion_tokens?: number
36+
completion_tokens_details?: {
37+
reasoning_tokens?: number
38+
}
3539
prompt_tokens?: number
40+
prompt_tokens_details?: {
41+
cached_tokens?: number
42+
}
3643
total_tokens?: number
3744
cost?: number
38-
39-
/**
40-
* Breakdown of tokens used in a completion.
41-
*/
42-
// completion_tokens_details?: CompletionUsage.CompletionTokensDetails;
43-
44-
/**
45-
* Breakdown of tokens used in the prompt.
46-
*/
47-
// prompt_tokens_details?: CompletionUsage.PromptTokensDetails;
4845
}
4946

5047
export class OpenRouterHandler extends BaseProvider implements SingleCompletionHandler {
@@ -160,16 +157,17 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
160157

161158
const delta = chunk.choices[0]?.delta
162159

163-
if ("reasoning" in delta && delta.reasoning) {
164-
yield { type: "reasoning", text: delta.reasoning } as ApiStreamChunk
160+
if ("reasoning" in delta && delta.reasoning && typeof delta.reasoning === "string") {
161+
yield { type: "reasoning", text: delta.reasoning }
165162
}
166163

167164
if (delta?.content) {
168165
fullResponseText += delta.content
169-
yield { type: "text", text: delta.content } as ApiStreamChunk
166+
yield { type: "text", text: delta.content }
170167
}
171168

172169
if (chunk.usage) {
170+
console.log("chunk.usage", chunk.usage)
173171
lastUsage = chunk.usage
174172
}
175173
}
@@ -179,7 +177,11 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
179177
type: "usage",
180178
inputTokens: lastUsage.prompt_tokens || 0,
181179
outputTokens: lastUsage.completion_tokens || 0,
182-
totalCost: lastUsage?.cost || 0,
180+
// Waiting on OpenRouter to figure out what this represents in the Gemini case
181+
// and how to best support it.
182+
// cacheReadTokens: lastUsage.prompt_tokens_details?.cached_tokens,
183+
reasoningTokens: lastUsage.completion_tokens_details?.reasoning_tokens,
184+
totalCost: lastUsage.cost || 0,
183185
}
184186
}
185187
}

src/api/transform/stream.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export type ApiStream = AsyncGenerator<ApiStreamChunk>
2+
23
export type ApiStreamChunk = ApiStreamTextChunk | ApiStreamUsageChunk | ApiStreamReasoningChunk
34

45
export interface ApiStreamTextChunk {
@@ -17,5 +18,6 @@ export interface ApiStreamUsageChunk {
1718
outputTokens: number
1819
cacheWriteTokens?: number
1920
cacheReadTokens?: number
20-
totalCost?: number // openrouter
21+
reasoningTokens?: number
22+
totalCost?: number
2123
}

webview-ui/src/components/chat/ChatRow.tsx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { ReasoningBlock } from "./ReasoningBlock"
2121
import Thumbnails from "../common/Thumbnails"
2222
import McpResourceRow from "../mcp/McpResourceRow"
2323
import McpToolRow from "../mcp/McpToolRow"
24-
import { highlightMentions } from "./TaskHeader"
24+
import { Mention } from "./Mention"
2525
import { CheckpointSaved } from "./checkpoints/CheckpointSaved"
2626
import { FollowUpSuggest } from "./FollowUpSuggest"
2727

@@ -867,7 +867,9 @@ export const ChatRowContent = ({
867867
return (
868868
<div className="bg-vscode-editor-background border rounded-xs p-1 overflow-hidden whitespace-pre-wrap word-break-break-word overflow-wrap-anywhere">
869869
<div className="flex justify-between gap-2">
870-
<div className="flex-grow px-2 py-1">{highlightMentions(message.text)}</div>
870+
<div className="flex-grow px-2 py-1">
871+
<Mention text={message.text} withShadow />
872+
</div>
871873
<Button
872874
variant="ghost"
873875
size="icon"
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { useMemo } from "react"
2+
import { useTranslation } from "react-i18next"
3+
4+
import { formatLargeNumber } from "@/utils/format"
5+
import { calculateTokenDistribution } from "@/utils/model-utils"
6+
7+
interface ContextWindowProgressProps {
8+
contextWindow: number
9+
contextTokens: number
10+
maxTokens?: number
11+
}
12+
13+
export const ContextWindowProgress = ({ contextWindow, contextTokens, maxTokens }: ContextWindowProgressProps) => {
14+
const { t } = useTranslation()
15+
// Use the shared utility function to calculate all token distribution values
16+
const tokenDistribution = useMemo(
17+
() => calculateTokenDistribution(contextWindow, contextTokens, maxTokens),
18+
[contextWindow, contextTokens, maxTokens],
19+
)
20+
21+
// Destructure the values we need
22+
const { currentPercent, reservedPercent, availableSize, reservedForOutput, availablePercent } = tokenDistribution
23+
24+
// For display purposes
25+
const safeContextWindow = Math.max(0, contextWindow)
26+
const safeContextTokens = Math.max(0, contextTokens)
27+
28+
return (
29+
<>
30+
<div className="flex items-center gap-2 flex-1 whitespace-nowrap px-2">
31+
<div data-testid="context-tokens-count">{formatLargeNumber(safeContextTokens)}</div>
32+
<div className="flex-1 relative">
33+
{/* Invisible overlay for hover area */}
34+
<div
35+
className="absolute w-full h-4 -top-[7px] z-5"
36+
title={t("chat:tokenProgress.availableSpace", { amount: formatLargeNumber(availableSize) })}
37+
data-testid="context-available-space"
38+
/>
39+
40+
{/* Main progress bar container */}
41+
<div className="flex items-center h-1 rounded-[2px] overflow-hidden w-full bg-[color-mix(in_srgb,var(--vscode-foreground)_20%,transparent)]">
42+
{/* Current tokens container */}
43+
<div className="relative h-full" style={{ width: `${currentPercent}%` }}>
44+
{/* Invisible overlay for current tokens section */}
45+
<div
46+
className="absolute h-4 -top-[7px] w-full z-6"
47+
title={t("chat:tokenProgress.tokensUsed", {
48+
used: formatLargeNumber(safeContextTokens),
49+
total: formatLargeNumber(safeContextWindow),
50+
})}
51+
data-testid="context-tokens-used"
52+
/>
53+
{/* Current tokens used - darkest */}
54+
<div className="h-full w-full bg-[var(--vscode-foreground)] transition-width duration-300 ease-out" />
55+
</div>
56+
57+
{/* Container for reserved tokens */}
58+
<div className="relative h-full" style={{ width: `${reservedPercent}%` }}>
59+
{/* Invisible overlay for reserved section */}
60+
<div
61+
className="absolute h-4 -top-[7px] w-full z-6"
62+
title={t("chat:tokenProgress.reservedForResponse", {
63+
amount: formatLargeNumber(reservedForOutput),
64+
})}
65+
data-testid="context-reserved-tokens"
66+
/>
67+
{/* Reserved for output section - medium gray */}
68+
<div className="h-full w-full bg-[color-mix(in_srgb,var(--vscode-foreground)_30%,transparent)] transition-width duration-300 ease-out" />
69+
</div>
70+
71+
{/* Empty section (if any) */}
72+
{availablePercent > 0 && (
73+
<div className="relative h-full" style={{ width: `${availablePercent}%` }}>
74+
{/* Invisible overlay for available space */}
75+
<div
76+
className="absolute h-4 -top-[7px] w-full z-6"
77+
title={t("chat:tokenProgress.availableSpace", {
78+
amount: formatLargeNumber(availableSize),
79+
})}
80+
data-testid="context-available-space-section"
81+
/>
82+
</div>
83+
)}
84+
</div>
85+
</div>
86+
<div data-testid="context-window-size">{formatLargeNumber(safeContextWindow)}</div>
87+
</div>
88+
</>
89+
)
90+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { mentionRegexGlobal } from "@roo/shared/context-mentions"
2+
3+
import { vscode } from "../../utils/vscode"
4+
5+
interface MentionProps {
6+
text?: string
7+
withShadow?: boolean
8+
}
9+
10+
export const Mention = ({ text, withShadow = false }: MentionProps) => {
11+
if (!text) {
12+
return <>{text}</>
13+
}
14+
15+
const parts = text.split(mentionRegexGlobal).map((part, index) => {
16+
if (index % 2 === 0) {
17+
// This is regular text.
18+
return part
19+
} else {
20+
// This is a mention.
21+
return (
22+
<span
23+
key={index}
24+
className={`${withShadow ? "mention-context-highlight-with-shadow" : "mention-context-highlight"} cursor-pointer`}
25+
onClick={() => vscode.postMessage({ type: "openMention", text: part })}>
26+
@{part}
27+
</span>
28+
)
29+
}
30+
})
31+
32+
return <>{parts}</>
33+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { useState } from "react"
2+
import prettyBytes from "pretty-bytes"
3+
import { useTranslation } from "react-i18next"
4+
5+
import { vscode } from "@/utils/vscode"
6+
import { Button } from "@/components/ui"
7+
8+
import { HistoryItem } from "@roo/shared/HistoryItem"
9+
10+
import { DeleteTaskDialog } from "../history/DeleteTaskDialog"
11+
12+
export const TaskActions = ({ item }: { item: HistoryItem | undefined }) => {
13+
const [deleteTaskId, setDeleteTaskId] = useState<string | null>(null)
14+
const { t } = useTranslation()
15+
16+
return (
17+
<div className="flex flex-row gap-1">
18+
<Button
19+
variant="ghost"
20+
size="sm"
21+
title={t("chat:task.export")}
22+
onClick={() => vscode.postMessage({ type: "exportCurrentTask" })}>
23+
<span className="codicon codicon-desktop-download" />
24+
</Button>
25+
{!!item?.size && item.size > 0 && (
26+
<>
27+
<Button
28+
variant="ghost"
29+
size="sm"
30+
title={t("chat:task.delete")}
31+
onClick={(e) => {
32+
e.stopPropagation()
33+
34+
if (e.shiftKey) {
35+
vscode.postMessage({ type: "deleteTaskWithId", text: item.id })
36+
} else {
37+
setDeleteTaskId(item.id)
38+
}
39+
}}>
40+
<span className="codicon codicon-trash" />
41+
{prettyBytes(item.size)}
42+
</Button>
43+
{deleteTaskId && (
44+
<DeleteTaskDialog
45+
taskId={deleteTaskId}
46+
onOpenChange={(open) => !open && setDeleteTaskId(null)}
47+
open
48+
/>
49+
)}
50+
</>
51+
)}
52+
</div>
53+
)
54+
}

0 commit comments

Comments
 (0)