diff --git a/src/shared/ExtensionMessage.ts b/src/shared/ExtensionMessage.ts index c6efdc1aea..f57435738e 100644 --- a/src/shared/ExtensionMessage.ts +++ b/src/shared/ExtensionMessage.ts @@ -18,6 +18,20 @@ import { Mode } from "./modes" import { RouterModels } from "./api" import { MarketplaceItem } from "../services/marketplace/types" +// Indexing status types +export interface IndexingStatus { + systemStatus: string + message?: string + processedItems: number + totalItems: number + currentItemUnit?: string +} + +export interface IndexingStatusUpdateMessage { + type: "indexingStatusUpdate" + values: IndexingStatus +} + export interface LanguageModelChatSelector { vendor?: string family?: string diff --git a/webview-ui/src/components/chat/ChatTextArea.tsx b/webview-ui/src/components/chat/ChatTextArea.tsx index 5e51edadce..006c525d70 100644 --- a/webview-ui/src/components/chat/ChatTextArea.tsx +++ b/webview-ui/src/components/chat/ChatTextArea.tsx @@ -26,6 +26,7 @@ import { MAX_IMAGES_PER_MESSAGE } from "./ChatView" import ContextMenu from "./ContextMenu" import { VolumeX, Pin, Check } from "lucide-react" import { IconButton } from "./IconButton" +import { IndexingStatusDot } from "./IndexingStatusBadge" import { cn } from "@/lib/utils" import { usePromptHistory } from "./hooks/usePromptHistory" @@ -78,6 +79,7 @@ const ChatTextArea = forwardRef( togglePinnedApiConfig, taskHistory, clineMessages, + codebaseIndexConfig, } = useExtensionState() // Find the ID and display text for the currently selected API configuration @@ -1171,6 +1173,7 @@ const ChatTextArea = forwardRef(
+ {codebaseIndexConfig?.codebaseIndexEnabled && } = ({ className }) => { + const { t } = useAppTranslation() + const { showTooltip, handleMouseEnter, handleMouseLeave, cleanup } = useTooltip({ delay: 300 }) + const [isHovered, setIsHovered] = useState(false) + + const [indexingStatus, setIndexingStatus] = useState({ + systemStatus: "Standby", + processedItems: 0, + totalItems: 0, + currentItemUnit: "items", + }) + + useEffect(() => { + // Request initial indexing status + vscode.postMessage({ type: "requestIndexingStatus" }) + + // Set up message listener for status updates + const handleMessage = (event: MessageEvent) => { + if (event.data.type === "indexingStatusUpdate") { + const status = event.data.values + setIndexingStatus(status) + } + } + + window.addEventListener("message", handleMessage) + + return () => { + window.removeEventListener("message", handleMessage) + cleanup() + } + }, [cleanup]) + + // Calculate progress percentage with memoization + const progressPercentage = useMemo( + () => + indexingStatus.totalItems > 0 + ? Math.round((indexingStatus.processedItems / indexingStatus.totalItems) * 100) + : 0, + [indexingStatus.processedItems, indexingStatus.totalItems], + ) + + // Get tooltip text with internationalization + const getTooltipText = () => { + switch (indexingStatus.systemStatus) { + case "Standby": + return t("chat:indexingStatus.ready") + case "Indexing": + return t("chat:indexingStatus.indexing", { percentage: progressPercentage }) + case "Indexed": + return t("chat:indexingStatus.indexed") + case "Error": + return t("chat:indexingStatus.error") + default: + return t("chat:indexingStatus.status") + } + } + + // Navigate to settings when clicked + const handleClick = () => { + window.postMessage( + { + type: "action", + action: "settingsButtonClicked", + values: { section: "experimental" }, + }, + "*", + ) + } + + const handleMouseEnterButton = () => { + setIsHovered(true) + handleMouseEnter() + } + + const handleMouseLeaveButton = () => { + setIsHovered(false) + handleMouseLeave() + } + + // Get status color classes based on status and hover state + const getStatusColorClass = () => { + const statusColors = { + Standby: { + default: "bg-vscode-descriptionForeground/40", + hover: "bg-vscode-descriptionForeground/60", + }, + Indexing: { + default: "bg-yellow-500/40 animate-pulse", + hover: "bg-yellow-500 animate-pulse", + }, + Indexed: { + default: "bg-green-500/40", + hover: "bg-green-500", + }, + Error: { + default: "bg-red-500/40", + hover: "bg-red-500", + }, + } + + const colors = statusColors[indexingStatus.systemStatus as keyof typeof statusColors] || statusColors.Standby + return isHovered ? colors.hover : colors.default + } + + return ( +
+ + {showTooltip && ( +
+ {getTooltipText()} +
+
+ )} +
+ ) +} diff --git a/webview-ui/src/components/chat/__tests__/ChatTextArea.test.tsx b/webview-ui/src/components/chat/__tests__/ChatTextArea.test.tsx index 7f01245144..6d07849938 100644 --- a/webview-ui/src/components/chat/__tests__/ChatTextArea.test.tsx +++ b/webview-ui/src/components/chat/__tests__/ChatTextArea.test.tsx @@ -126,6 +126,9 @@ describe("ChatTextArea", () => { render() + // Clear any calls from component initialization (e.g., IndexingStatusBadge) + mockPostMessage.mockClear() + const enhanceButton = getEnhancePromptButton() fireEvent.click(enhanceButton) diff --git a/webview-ui/src/components/chat/__tests__/IndexingStatusBadge.test.tsx b/webview-ui/src/components/chat/__tests__/IndexingStatusBadge.test.tsx new file mode 100644 index 0000000000..4297c485d5 --- /dev/null +++ b/webview-ui/src/components/chat/__tests__/IndexingStatusBadge.test.tsx @@ -0,0 +1,278 @@ +import React from "react" +import { render, screen, fireEvent, waitFor, act } from "@testing-library/react" +import { IndexingStatusDot } from "../IndexingStatusBadge" +import { vscode } from "@src/utils/vscode" + +// Mock i18n setup to prevent initialization errors +jest.mock("@/i18n/setup", () => ({ + __esModule: true, + default: { + use: jest.fn().mockReturnThis(), + init: jest.fn().mockReturnThis(), + addResourceBundle: jest.fn(), + language: "en", + changeLanguage: jest.fn(), + }, + loadTranslations: jest.fn(), +})) + +// Mock react-i18next +jest.mock("react-i18next", () => ({ + useTranslation: () => ({ + t: (key: string, params?: any) => { + const translations: Record = { + "indexingStatus.ready": "Index ready", + "indexingStatus.indexing": params?.progress !== undefined ? `Indexing ${params.progress}%` : "Indexing", + "indexingStatus.error": "Index error", + "indexingStatus.indexed": "Indexed", + "indexingStatus.tooltip.ready": "The codebase index is ready for use", + "indexingStatus.tooltip.indexing": + params?.progress !== undefined + ? `Indexing in progress: ${params.progress}% complete` + : "Indexing in progress", + "indexingStatus.tooltip.error": "An error occurred during indexing", + "indexingStatus.tooltip.indexed": "Codebase has been successfully indexed", + "indexingStatus.tooltip.clickToSettings": "Click to open indexing settings", + } + return translations[key] || key + }, + i18n: { + language: "en", + changeLanguage: jest.fn(), + }, + }), + initReactI18next: { + type: "3rdParty", + init: jest.fn(), + }, +})) + +// Mock vscode API +jest.mock("@src/utils/vscode", () => ({ + vscode: { + postMessage: jest.fn(), + }, +})) + +// Mock the useTooltip hook +jest.mock("@/hooks/useTooltip", () => ({ + useTooltip: jest.fn(() => ({ + showTooltip: false, + handleMouseEnter: jest.fn(), + handleMouseLeave: jest.fn(), + cleanup: jest.fn(), + })), +})) + +// Mock the ExtensionStateContext +jest.mock("@/context/ExtensionStateContext", () => ({ + useExtensionState: () => ({ + version: "1.0.0", + clineMessages: [], + taskHistory: [], + shouldShowAnnouncement: false, + language: "en", + }), + ExtensionStateContextProvider: ({ children }: { children: React.ReactNode }) => <>{children}, +})) + +// Mock TranslationContext to provide t function directly +jest.mock("@/i18n/TranslationContext", () => ({ + useAppTranslation: () => ({ + t: (key: string, params?: any) => { + // Remove namespace prefix if present + const cleanKey = key.includes(":") ? key.split(":")[1] : key + + const translations: Record = { + "indexingStatus.ready": "Index ready", + "indexingStatus.indexing": + params?.percentage !== undefined ? `Indexing ${params.percentage}%` : "Indexing", + "indexingStatus.error": "Index error", + "indexingStatus.indexed": "Indexed", + "indexingStatus.tooltip.ready": "The codebase index is ready for use", + "indexingStatus.tooltip.indexing": + params?.percentage !== undefined + ? `Indexing in progress: ${params.percentage}% complete` + : "Indexing in progress", + "indexingStatus.tooltip.error": "An error occurred during indexing", + "indexingStatus.tooltip.indexed": "Codebase has been successfully indexed", + "indexingStatus.tooltip.clickToSettings": "Click to open indexing settings", + } + return translations[cleanKey] || cleanKey + }, + }), +})) + +describe("IndexingStatusDot", () => { + const renderComponent = (props = {}) => { + return render() + } + + beforeEach(() => { + jest.clearAllMocks() + }) + + it("renders the status dot", () => { + renderComponent() + const button = screen.getByRole("button") + expect(button).toBeInTheDocument() + }) + + it("shows standby status by default", () => { + renderComponent() + const button = screen.getByRole("button") + expect(button).toHaveAttribute("aria-label", "Index ready") + }) + + it("posts settingsButtonClicked message when clicked", () => { + // Mock window.postMessage + const postMessageSpy = jest.spyOn(window, "postMessage") + + renderComponent() + + const button = screen.getByRole("button") + fireEvent.click(button) + + expect(postMessageSpy).toHaveBeenCalledWith( + { + type: "action", + action: "settingsButtonClicked", + values: { section: "experimental" }, + }, + "*", + ) + + postMessageSpy.mockRestore() + }) + + it("requests indexing status on mount", () => { + renderComponent() + + expect(vscode.postMessage).toHaveBeenCalledWith({ + type: "requestIndexingStatus", + }) + }) + + it("updates status when receiving indexingStatusUpdate message", async () => { + renderComponent() + + // Simulate receiving an indexing status update + const event = new MessageEvent("message", { + data: { + type: "indexingStatusUpdate", + values: { + systemStatus: "Indexing", + processedItems: 50, + totalItems: 100, + currentItemUnit: "files", + }, + }, + }) + + act(() => { + window.dispatchEvent(event) + }) + + await waitFor(() => { + const button = screen.getByRole("button") + expect(button).toHaveAttribute("aria-label", "Indexing 50%") + }) + }) + + it("shows error status correctly", async () => { + renderComponent() + + // Simulate error status + const event = new MessageEvent("message", { + data: { + type: "indexingStatusUpdate", + values: { + systemStatus: "Error", + processedItems: 0, + totalItems: 0, + currentItemUnit: "files", + }, + }, + }) + + act(() => { + window.dispatchEvent(event) + }) + + await waitFor(() => { + const button = screen.getByRole("button") + expect(button).toHaveAttribute("aria-label", "Index error") + }) + }) + + it("shows indexed status correctly", async () => { + renderComponent() + + // Simulate indexed status + const event = new MessageEvent("message", { + data: { + type: "indexingStatusUpdate", + values: { + systemStatus: "Indexed", + processedItems: 100, + totalItems: 100, + currentItemUnit: "files", + }, + }, + }) + + act(() => { + window.dispatchEvent(event) + }) + + await waitFor(() => { + const button = screen.getByRole("button") + expect(button).toHaveAttribute("aria-label", "Indexed") + }) + }) + + it("cleans up event listener on unmount", () => { + const { unmount } = renderComponent() + const removeEventListenerSpy = jest.spyOn(window, "removeEventListener") + + unmount() + + expect(removeEventListenerSpy).toHaveBeenCalledWith("message", expect.any(Function)) + }) + + it("calculates progress percentage correctly", async () => { + renderComponent() + + // Test various progress scenarios + const testCases = [ + { processed: 0, total: 100, expected: 0 }, + { processed: 25, total: 100, expected: 25 }, + { processed: 33, total: 100, expected: 33 }, + { processed: 100, total: 100, expected: 100 }, + { processed: 0, total: 0, expected: 0 }, + ] + + for (const testCase of testCases) { + const event = new MessageEvent("message", { + data: { + type: "indexingStatusUpdate", + values: { + systemStatus: "Indexing", + processedItems: testCase.processed, + totalItems: testCase.total, + currentItemUnit: "files", + }, + }, + }) + + act(() => { + window.dispatchEvent(event) + }) + + await waitFor(() => { + const button = screen.getByRole("button") + expect(button).toHaveAttribute("aria-label", `Indexing ${testCase.expected}%`) + }) + } + }) +}) diff --git a/webview-ui/src/components/settings/CodeIndexSettings.tsx b/webview-ui/src/components/settings/CodeIndexSettings.tsx index 2bfec203da..13c9524d9f 100644 --- a/webview-ui/src/components/settings/CodeIndexSettings.tsx +++ b/webview-ui/src/components/settings/CodeIndexSettings.tsx @@ -40,16 +40,7 @@ interface CodeIndexSettingsProps { areSettingsCommitted: boolean } -interface IndexingStatusUpdateMessage { - type: "indexingStatusUpdate" - values: { - systemStatus: string - message?: string - processedItems: number - totalItems: number - currentItemUnit?: string - } -} +import type { IndexingStatusUpdateMessage } from "@roo/ExtensionMessage" export const CodeIndexSettings: React.FC = ({ codebaseIndexModels, diff --git a/webview-ui/src/hooks/useTooltip.ts b/webview-ui/src/hooks/useTooltip.ts new file mode 100644 index 0000000000..f098017acf --- /dev/null +++ b/webview-ui/src/hooks/useTooltip.ts @@ -0,0 +1,39 @@ +import { useState, useCallback, useRef } from "react" + +interface UseTooltipOptions { + delay?: number +} + +export const useTooltip = (options: UseTooltipOptions = {}) => { + const { delay = 300 } = options + const [showTooltip, setShowTooltip] = useState(false) + const timeoutRef = useRef(null) + + const handleMouseEnter = useCallback(() => { + if (timeoutRef.current) clearTimeout(timeoutRef.current) + timeoutRef.current = setTimeout(() => setShowTooltip(true), delay) + }, [delay]) + + const handleMouseLeave = useCallback(() => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + timeoutRef.current = null + } + setShowTooltip(false) + }, []) + + // Cleanup on unmount + const cleanup = useCallback(() => { + if (timeoutRef.current) { + clearTimeout(timeoutRef.current) + timeoutRef.current = null + } + }, []) + + return { + showTooltip, + handleMouseEnter, + handleMouseLeave, + cleanup, + } +} diff --git a/webview-ui/src/i18n/locales/ca/chat.json b/webview-ui/src/i18n/locales/ca/chat.json index b3cf1e41b6..4d8885b3cd 100644 --- a/webview-ui/src/i18n/locales/ca/chat.json +++ b/webview-ui/src/i18n/locales/ca/chat.json @@ -279,5 +279,12 @@ "deny": { "title": "Denegar tot" } + }, + "indexingStatus": { + "ready": "Índex preparat", + "indexing": "Indexant {{percentage}}%", + "indexed": "Indexat", + "error": "Error d'índex", + "status": "Estat de l'índex" } } diff --git a/webview-ui/src/i18n/locales/de/chat.json b/webview-ui/src/i18n/locales/de/chat.json index 5b1edb2939..af441b5dee 100644 --- a/webview-ui/src/i18n/locales/de/chat.json +++ b/webview-ui/src/i18n/locales/de/chat.json @@ -279,5 +279,12 @@ "deny": { "title": "Alle ablehnen" } + }, + "indexingStatus": { + "ready": "Index bereit", + "indexing": "Indizierung {{percentage}}%", + "indexed": "Indiziert", + "error": "Index-Fehler", + "status": "Index-Status" } } diff --git a/webview-ui/src/i18n/locales/en/chat.json b/webview-ui/src/i18n/locales/en/chat.json index 866f589a8a..df4659f5cc 100644 --- a/webview-ui/src/i18n/locales/en/chat.json +++ b/webview-ui/src/i18n/locales/en/chat.json @@ -279,5 +279,12 @@ "description": "Roo has reached the auto-approved limit of {{count}} API request(s). Would you like to reset the count and proceed with the task?", "button": "Reset and Continue" } + }, + "indexingStatus": { + "ready": "Index ready", + "indexing": "Indexing {{percentage}}%", + "indexed": "Indexed", + "error": "Index error", + "status": "Index status" } } diff --git a/webview-ui/src/i18n/locales/es/chat.json b/webview-ui/src/i18n/locales/es/chat.json index af3c3ab24e..ab75e33c50 100644 --- a/webview-ui/src/i18n/locales/es/chat.json +++ b/webview-ui/src/i18n/locales/es/chat.json @@ -279,5 +279,12 @@ "deny": { "title": "Denegar todo" } + }, + "indexingStatus": { + "ready": "Índice listo", + "indexing": "Indexando {{percentage}}%", + "indexed": "Indexado", + "error": "Error de índice", + "status": "Estado del índice" } } diff --git a/webview-ui/src/i18n/locales/fr/chat.json b/webview-ui/src/i18n/locales/fr/chat.json index 3b305b4397..cd40320cde 100644 --- a/webview-ui/src/i18n/locales/fr/chat.json +++ b/webview-ui/src/i18n/locales/fr/chat.json @@ -279,5 +279,12 @@ "deny": { "title": "Tout refuser" } + }, + "indexingStatus": { + "ready": "Index prêt", + "indexing": "Indexation {{percentage}}%", + "indexed": "Indexé", + "error": "Erreur d'index", + "status": "Statut de l'index" } } diff --git a/webview-ui/src/i18n/locales/hi/chat.json b/webview-ui/src/i18n/locales/hi/chat.json index 075980435f..9d15cef68b 100644 --- a/webview-ui/src/i18n/locales/hi/chat.json +++ b/webview-ui/src/i18n/locales/hi/chat.json @@ -279,5 +279,12 @@ "deny": { "title": "सभी अस्वीकार करें" } + }, + "indexingStatus": { + "ready": "इंडेक्स तैयार", + "indexing": "इंडेक्सिंग {{percentage}}%", + "indexed": "इंडेक्स किया गया", + "error": "इंडेक्स त्रुटि", + "status": "इंडेक्स स्थिति" } } diff --git a/webview-ui/src/i18n/locales/it/chat.json b/webview-ui/src/i18n/locales/it/chat.json index a28fa85850..4680990382 100644 --- a/webview-ui/src/i18n/locales/it/chat.json +++ b/webview-ui/src/i18n/locales/it/chat.json @@ -279,5 +279,12 @@ "deny": { "title": "Nega tutto" } + }, + "indexingStatus": { + "ready": "Indice pronto", + "indexing": "Indicizzazione {{percentage}}%", + "indexed": "Indicizzato", + "error": "Errore indice", + "status": "Stato indice" } } diff --git a/webview-ui/src/i18n/locales/ja/chat.json b/webview-ui/src/i18n/locales/ja/chat.json index dc60077f86..963c57fa99 100644 --- a/webview-ui/src/i18n/locales/ja/chat.json +++ b/webview-ui/src/i18n/locales/ja/chat.json @@ -279,5 +279,12 @@ "deny": { "title": "すべて拒否" } + }, + "indexingStatus": { + "ready": "インデックス準備完了", + "indexing": "インデックス作成中 {{percentage}}%", + "indexed": "インデックス作成済み", + "error": "インデックスエラー", + "status": "インデックス状態" } } diff --git a/webview-ui/src/i18n/locales/ko/chat.json b/webview-ui/src/i18n/locales/ko/chat.json index 2afe6ff5f4..bc0a4411ab 100644 --- a/webview-ui/src/i18n/locales/ko/chat.json +++ b/webview-ui/src/i18n/locales/ko/chat.json @@ -279,5 +279,12 @@ "deny": { "title": "모두 거부" } + }, + "indexingStatus": { + "ready": "인덱스 준비됨", + "indexing": "인덱싱 중 {{percentage}}%", + "indexed": "인덱싱 완료", + "error": "인덱스 오류", + "status": "인덱스 상태" } } diff --git a/webview-ui/src/i18n/locales/nl/chat.json b/webview-ui/src/i18n/locales/nl/chat.json index 5001b9f7cc..8bd0adba8e 100644 --- a/webview-ui/src/i18n/locales/nl/chat.json +++ b/webview-ui/src/i18n/locales/nl/chat.json @@ -279,5 +279,12 @@ "deny": { "title": "Alles weigeren" } + }, + "indexingStatus": { + "ready": "Index gereed", + "indexing": "Indexeren {{percentage}}%", + "indexed": "Geïndexeerd", + "error": "Index fout", + "status": "Index status" } } diff --git a/webview-ui/src/i18n/locales/pl/chat.json b/webview-ui/src/i18n/locales/pl/chat.json index c02a9ba777..cb9d5df300 100644 --- a/webview-ui/src/i18n/locales/pl/chat.json +++ b/webview-ui/src/i18n/locales/pl/chat.json @@ -279,5 +279,12 @@ "deny": { "title": "Odrzuć wszystko" } + }, + "indexingStatus": { + "ready": "Indeks gotowy", + "indexing": "Indeksowanie {{percentage}}%", + "indexed": "Zaindeksowane", + "error": "Błąd indeksu", + "status": "Status indeksu" } } diff --git a/webview-ui/src/i18n/locales/pt-BR/chat.json b/webview-ui/src/i18n/locales/pt-BR/chat.json index f1eca6a982..a1dec8f0ec 100644 --- a/webview-ui/src/i18n/locales/pt-BR/chat.json +++ b/webview-ui/src/i18n/locales/pt-BR/chat.json @@ -279,5 +279,12 @@ "deny": { "title": "Negar tudo" } + }, + "indexingStatus": { + "ready": "Índice pronto", + "indexing": "Indexando {{percentage}}%", + "indexed": "Indexado", + "error": "Erro do índice", + "status": "Status do índice" } } diff --git a/webview-ui/src/i18n/locales/ru/chat.json b/webview-ui/src/i18n/locales/ru/chat.json index fcee847f15..6cce050d9b 100644 --- a/webview-ui/src/i18n/locales/ru/chat.json +++ b/webview-ui/src/i18n/locales/ru/chat.json @@ -279,5 +279,12 @@ "deny": { "title": "Отклонить все" } + }, + "indexingStatus": { + "ready": "Индекс готов", + "indexing": "Индексация {{percentage}}%", + "indexed": "Проиндексировано", + "error": "Ошибка индекса", + "status": "Статус индекса" } } diff --git a/webview-ui/src/i18n/locales/tr/chat.json b/webview-ui/src/i18n/locales/tr/chat.json index a3a0a9bfd4..5fe727b1ce 100644 --- a/webview-ui/src/i18n/locales/tr/chat.json +++ b/webview-ui/src/i18n/locales/tr/chat.json @@ -279,5 +279,12 @@ "deny": { "title": "Tümünü Reddet" } + }, + "indexingStatus": { + "ready": "İndeks hazır", + "indexing": "İndeksleniyor {{percentage}}%", + "indexed": "İndekslendi", + "error": "İndeks hatası", + "status": "İndeks durumu" } } diff --git a/webview-ui/src/i18n/locales/vi/chat.json b/webview-ui/src/i18n/locales/vi/chat.json index 3349f20002..8673c9d80c 100644 --- a/webview-ui/src/i18n/locales/vi/chat.json +++ b/webview-ui/src/i18n/locales/vi/chat.json @@ -279,5 +279,12 @@ "deny": { "title": "Từ chối tất cả" } + }, + "indexingStatus": { + "ready": "Chỉ mục sẵn sàng", + "indexing": "Đang lập chỉ mục {{percentage}}%", + "indexed": "Đã lập chỉ mục", + "error": "Lỗi chỉ mục", + "status": "Trạng thái chỉ mục" } } diff --git a/webview-ui/src/i18n/locales/zh-CN/chat.json b/webview-ui/src/i18n/locales/zh-CN/chat.json index 3841abd499..089cfa8085 100644 --- a/webview-ui/src/i18n/locales/zh-CN/chat.json +++ b/webview-ui/src/i18n/locales/zh-CN/chat.json @@ -279,5 +279,12 @@ "deny": { "title": "全部拒绝" } + }, + "indexingStatus": { + "ready": "索引就绪", + "indexing": "索引中 {{percentage}}%", + "indexed": "已索引", + "error": "索引错误", + "status": "索引状态" } } diff --git a/webview-ui/src/i18n/locales/zh-TW/chat.json b/webview-ui/src/i18n/locales/zh-TW/chat.json index 651719e839..e426354714 100644 --- a/webview-ui/src/i18n/locales/zh-TW/chat.json +++ b/webview-ui/src/i18n/locales/zh-TW/chat.json @@ -279,5 +279,12 @@ "deny": { "title": "全部拒絕" } + }, + "indexingStatus": { + "ready": "索引就緒", + "indexing": "索引中 {{percentage}}%", + "indexed": "已索引", + "error": "索引錯誤", + "status": "索引狀態" } }