Skip to content

Commit 4e5eb1b

Browse files
hannesrudolphdaniel-lxs
authored andcommitted
feat: Add indexing status badge to chat view (#4429) (#4532)
Co-authored-by: Daniel <[email protected]> Co-authored-by: Daniel Riccio <[email protected]>
1 parent 0e3e056 commit 4e5eb1b

File tree

24 files changed

+615
-10
lines changed

24 files changed

+615
-10
lines changed

src/shared/ExtensionMessage.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,20 @@ import { Mode } from "./modes"
1818
import { RouterModels } from "./api"
1919
import { MarketplaceItem } from "../services/marketplace/types"
2020

21+
// Indexing status types
22+
export interface IndexingStatus {
23+
systemStatus: string
24+
message?: string
25+
processedItems: number
26+
totalItems: number
27+
currentItemUnit?: string
28+
}
29+
30+
export interface IndexingStatusUpdateMessage {
31+
type: "indexingStatusUpdate"
32+
values: IndexingStatus
33+
}
34+
2135
export interface LanguageModelChatSelector {
2236
vendor?: string
2337
family?: string

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import { MAX_IMAGES_PER_MESSAGE } from "./ChatView"
2626
import ContextMenu from "./ContextMenu"
2727
import { VolumeX, Pin, Check } from "lucide-react"
2828
import { IconButton } from "./IconButton"
29+
import { IndexingStatusDot } from "./IndexingStatusBadge"
2930
import { cn } from "@/lib/utils"
3031
import { usePromptHistory } from "./hooks/usePromptHistory"
3132

@@ -78,6 +79,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
7879
togglePinnedApiConfig,
7980
taskHistory,
8081
clineMessages,
82+
codebaseIndexConfig,
8183
} = useExtensionState()
8284

8385
// Find the ID and display text for the currently selected API configuration
@@ -1171,6 +1173,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
11711173
</div>
11721174

11731175
<div className={cn("flex", "items-center", "gap-0.5", "shrink-0")}>
1176+
{codebaseIndexConfig?.codebaseIndexEnabled && <IndexingStatusDot />}
11741177
<IconButton
11751178
iconClass={isEnhancingPrompt ? "codicon-loading" : "codicon-sparkle"}
11761179
title={t("chat:enhancePrompt")}
Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
1+
import React, { useState, useEffect, useMemo } from "react"
2+
import { cn } from "@src/lib/utils"
3+
import { vscode } from "@src/utils/vscode"
4+
import { useAppTranslation } from "@/i18n/TranslationContext"
5+
import { useTooltip } from "@/hooks/useTooltip"
6+
import type { IndexingStatus, IndexingStatusUpdateMessage } from "@roo/ExtensionMessage"
7+
8+
interface IndexingStatusDotProps {
9+
className?: string
10+
}
11+
12+
export const IndexingStatusDot: React.FC<IndexingStatusDotProps> = ({ className }) => {
13+
const { t } = useAppTranslation()
14+
const { showTooltip, handleMouseEnter, handleMouseLeave, cleanup } = useTooltip({ delay: 300 })
15+
const [isHovered, setIsHovered] = useState(false)
16+
17+
const [indexingStatus, setIndexingStatus] = useState<IndexingStatus>({
18+
systemStatus: "Standby",
19+
processedItems: 0,
20+
totalItems: 0,
21+
currentItemUnit: "items",
22+
})
23+
24+
useEffect(() => {
25+
// Request initial indexing status
26+
vscode.postMessage({ type: "requestIndexingStatus" })
27+
28+
// Set up message listener for status updates
29+
const handleMessage = (event: MessageEvent<IndexingStatusUpdateMessage>) => {
30+
if (event.data.type === "indexingStatusUpdate") {
31+
const status = event.data.values
32+
setIndexingStatus(status)
33+
}
34+
}
35+
36+
window.addEventListener("message", handleMessage)
37+
38+
return () => {
39+
window.removeEventListener("message", handleMessage)
40+
cleanup()
41+
}
42+
}, [cleanup])
43+
44+
// Calculate progress percentage with memoization
45+
const progressPercentage = useMemo(
46+
() =>
47+
indexingStatus.totalItems > 0
48+
? Math.round((indexingStatus.processedItems / indexingStatus.totalItems) * 100)
49+
: 0,
50+
[indexingStatus.processedItems, indexingStatus.totalItems],
51+
)
52+
53+
// Get tooltip text with internationalization
54+
const getTooltipText = () => {
55+
switch (indexingStatus.systemStatus) {
56+
case "Standby":
57+
return t("chat:indexingStatus.ready")
58+
case "Indexing":
59+
return t("chat:indexingStatus.indexing", { percentage: progressPercentage })
60+
case "Indexed":
61+
return t("chat:indexingStatus.indexed")
62+
case "Error":
63+
return t("chat:indexingStatus.error")
64+
default:
65+
return t("chat:indexingStatus.status")
66+
}
67+
}
68+
69+
// Navigate to settings when clicked
70+
const handleClick = () => {
71+
window.postMessage(
72+
{
73+
type: "action",
74+
action: "settingsButtonClicked",
75+
values: { section: "experimental" },
76+
},
77+
"*",
78+
)
79+
}
80+
81+
const handleMouseEnterButton = () => {
82+
setIsHovered(true)
83+
handleMouseEnter()
84+
}
85+
86+
const handleMouseLeaveButton = () => {
87+
setIsHovered(false)
88+
handleMouseLeave()
89+
}
90+
91+
// Get status color classes based on status and hover state
92+
const getStatusColorClass = () => {
93+
const statusColors = {
94+
Standby: {
95+
default: "bg-vscode-descriptionForeground/40",
96+
hover: "bg-vscode-descriptionForeground/60",
97+
},
98+
Indexing: {
99+
default: "bg-yellow-500/40 animate-pulse",
100+
hover: "bg-yellow-500 animate-pulse",
101+
},
102+
Indexed: {
103+
default: "bg-green-500/40",
104+
hover: "bg-green-500",
105+
},
106+
Error: {
107+
default: "bg-red-500/40",
108+
hover: "bg-red-500",
109+
},
110+
}
111+
112+
const colors = statusColors[indexingStatus.systemStatus as keyof typeof statusColors] || statusColors.Standby
113+
return isHovered ? colors.hover : colors.default
114+
}
115+
116+
return (
117+
<div className={cn("relative inline-block", className)}>
118+
<button
119+
onClick={handleClick}
120+
onMouseEnter={handleMouseEnterButton}
121+
onMouseLeave={handleMouseLeaveButton}
122+
className={cn(
123+
"flex items-center justify-center w-7 h-7 rounded-md",
124+
"bg-transparent hover:bg-vscode-list-hoverBackground",
125+
"cursor-pointer transition-all duration-200",
126+
"opacity-85 hover:opacity-100 relative",
127+
)}
128+
aria-label={getTooltipText()}>
129+
{/* Status dot */}
130+
<span
131+
className={cn(
132+
"inline-block w-2 h-2 rounded-full relative z-10 transition-colors duration-200",
133+
getStatusColorClass(),
134+
)}
135+
/>
136+
</button>
137+
{showTooltip && (
138+
<div
139+
className={cn(
140+
"absolute bottom-full left-1/2 transform -translate-x-1/2 mb-2",
141+
"px-2 py-1 text-xs font-medium text-vscode-foreground",
142+
"bg-vscode-editor-background border border-vscode-panel-border",
143+
"rounded shadow-lg whitespace-nowrap z-50",
144+
)}
145+
role="tooltip">
146+
{getTooltipText()}
147+
<div
148+
className={cn(
149+
"absolute top-full left-1/2 transform -translate-x-1/2",
150+
"w-0 h-0 border-l-4 border-r-4 border-t-4",
151+
"border-l-transparent border-r-transparent border-t-vscode-panel-border",
152+
)}
153+
/>
154+
</div>
155+
)}
156+
</div>
157+
)
158+
}

webview-ui/src/components/chat/__tests__/ChatTextArea.test.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,9 @@ describe("ChatTextArea", () => {
126126

127127
render(<ChatTextArea {...defaultProps} inputValue="" />)
128128

129+
// Clear any calls from component initialization (e.g., IndexingStatusBadge)
130+
mockPostMessage.mockClear()
131+
129132
const enhanceButton = getEnhancePromptButton()
130133
fireEvent.click(enhanceButton)
131134

0 commit comments

Comments
 (0)