瀏覽代碼

fix(chat): 上下文用量条按窗口上限比例填充

分段条原先以已用 token 为 100%,导致半满时仍铺满整根。改为以 windowTokens 为总长度,未使用部分保留底色。

Co-authored-by: darknessomi <darknessomi@users.noreply.github.com>
Cursor Agent 3 周之前
父節點
當前提交
dc1d43f569
共有 2 個文件被更改,包括 60 次插入19 次删除
  1. 37 0
      src/components/chat/context-usage-ring.spec.tsx
  2. 23 19
      src/components/chat/context-usage-ring.tsx

+ 37 - 0
src/components/chat/context-usage-ring.spec.tsx

@@ -55,6 +55,28 @@ describe("ContextUsageRing", () => {
     container.remove()
   })
 
+  async function openTooltip() {
+    const trigger = container.querySelector("button")
+    expect(trigger).toBeTruthy()
+    await act(async () => {
+      trigger?.dispatchEvent(new MouseEvent("mouseenter", { bubbles: true }))
+      trigger?.focus()
+      trigger?.click()
+    })
+    await act(async () => {
+      await Promise.resolve()
+    })
+  }
+
+  function filledBarPercent(): number {
+    const bar = document.querySelector<HTMLElement>('[data-testid="context-usage-bar"]')
+    expect(bar).toBeTruthy()
+    return Array.from(bar!.children).reduce((sum, node) => {
+      const width = (node as HTMLElement).style.width
+      return sum + Number.parseFloat(width || "0")
+    }, 0)
+  }
+
   it("renders nothing without usage", async () => {
     await act(async () => {
       root.render(<ContextUsageRing />)
@@ -62,6 +84,21 @@ describe("ContextUsageRing", () => {
     expect(container.textContent).toBe("")
   })
 
+  it("fills the usage bar against the context window, not used tokens", async () => {
+    await act(async () => {
+      root.render(<ContextUsageRing usage={usage} />)
+    })
+    await openTooltip()
+
+    const filled = filledBarPercent()
+    expect(filled).toBeCloseTo((usage.totalTokens / usage.windowTokens) * 100, 5)
+    expect(filled).toBe(23)
+    expect(filled).toBeLessThan(100)
+
+    const history = document.querySelector<HTMLElement>('[data-segment="history"]')
+    expect(history?.style.width).toBe("4%")
+  })
+
   it("shows percent and warns when nearly full", async () => {
     const onCreateConversation = vi.fn()
     const fullUsage: ContextUsageSnapshot = {

+ 23 - 19
src/components/chat/context-usage-ring.tsx

@@ -40,22 +40,26 @@ function ringStrokeColor(ratio: number): string {
   return "#22c55e"
 }
 
-function SegmentBar({ segments, totalTokens }: {
+function SegmentBar({ segments, windowTokens }: {
   segments: ContextUsageSnapshot["segments"]
-  totalTokens: number
+  windowTokens: number
 }) {
   const ordered = CONTEXT_USAGE_SEGMENT_ORDER
     .map((key) => segments.find((segment) => segment.key === key))
     .filter((segment): segment is NonNullable<typeof segment> => Boolean(segment && segment.tokens > 0))
-  const denominator = Math.max(1, totalTokens)
+  const denominator = Math.max(1, windowTokens)
   return (
-    <div className="flex h-1.5 w-full overflow-hidden rounded-full bg-muted">
+    <div
+      data-testid="context-usage-bar"
+      className="flex h-2 w-full overflow-hidden rounded-full bg-muted"
+    >
       {ordered.map((segment) => (
         <div
           key={segment.key}
-          className="h-full"
+          className="h-full shrink-0"
+          data-segment={segment.key}
           style={{
-            width: `${Math.max(1, (segment.tokens / denominator) * 100)}%`,
+            width: `${(segment.tokens / denominator) * 100}%`,
             backgroundColor: SEGMENT_COLORS[segment.key] ?? "#94a3b8",
           }}
         />
@@ -142,24 +146,24 @@ export function ContextUsageRing({
           className="w-72 max-w-none border border-border bg-popover p-3 text-popover-foreground shadow-md"
         >
           <div className="space-y-2.5 text-left">
-            <div className="flex items-baseline justify-between gap-3">
-              <div>
-                <div className="text-xs font-medium">{t("chat.contextUsage.title")}</div>
-                <div className="mt-0.5 text-sm font-semibold">
+            <div className="space-y-1">
+              <div className="text-xs font-medium">{t("chat.contextUsage.title")}</div>
+              <div className="flex items-baseline justify-between gap-3">
+                <div className="text-sm font-semibold">
                   {t("chat.contextUsage.percentFull", { percent })}
                 </div>
-              </div>
-              <div className="text-xs text-muted-foreground">
-                {usage.estimated ? "~" : ""}
-                {formatContextTokenCount(usage.totalTokens)}
-                {" / "}
-                {formatContextTokenCount(usage.windowTokens)}
-                {" "}
-                {t("chat.contextUsage.tokens")}
+                <div className="text-xs text-muted-foreground">
+                  {usage.estimated ? "~" : ""}
+                  {formatContextTokenCount(usage.totalTokens)}
+                  {" / "}
+                  {formatContextTokenCount(usage.windowTokens)}
+                  {" "}
+                  {t("chat.contextUsage.tokens")}
+                </div>
               </div>
             </div>
 
-            <SegmentBar segments={usage.segments} totalTokens={usage.totalTokens} />
+            <SegmentBar segments={usage.segments} windowTokens={usage.windowTokens} />
 
             <div className="space-y-1.5">
               {segmentRows.map((row) => (