mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-06 08:18:39 +00:00
feat: replace static API Request label with customizable animated indicator
- Add AnimatedStatusIndicator component with pulsing animation - Support customizable status texts and emoji mode - Add settings UI for configuring the animated indicator - Include random mode to cycle through messages - Add CSS animations for subtle pulse effect - Integrate with ChatRow to replace static label Implements #7523
This commit is contained in:
parent
1d46bd1bbc
commit
4ed2a3d514
8 changed files with 435 additions and 3 deletions
|
|
@ -212,6 +212,7 @@ export interface WebviewMessage {
|
|||
| "createCommand"
|
||||
| "insertTextIntoTextarea"
|
||||
| "showMdmAuthRequiredNotification"
|
||||
| "apiStatusConfig"
|
||||
text?: string
|
||||
editedMessageContent?: string
|
||||
tab?: "settings" | "history" | "mcp" | "modes" | "chat" | "marketplace" | "account"
|
||||
|
|
|
|||
117
webview-ui/src/components/chat/AnimatedStatusIndicator.tsx
Normal file
117
webview-ui/src/components/chat/AnimatedStatusIndicator.tsx
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
import React, { useEffect, useState, useMemo } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { useExtensionState } from "@src/context/ExtensionStateContext"
|
||||
|
||||
interface AnimatedStatusIndicatorProps {
|
||||
isStreaming: boolean
|
||||
cost?: number | null
|
||||
cancelReason?: string | null
|
||||
apiRequestFailedMessage?: string
|
||||
streamingFailedMessage?: string
|
||||
}
|
||||
|
||||
const DEFAULT_STATUS_TEXTS = ["Generating...", "Thinking...", "Working on it...", "Processing...", "Analyzing..."]
|
||||
|
||||
const DEFAULT_EMOJIS = [
|
||||
"🤔", // Thinking
|
||||
"🧠", // Brainstorming
|
||||
"⏳", // Loading
|
||||
"✨", // Magic
|
||||
"🔮", // Summoning
|
||||
"💭", // Thought bubble
|
||||
"⚡", // Lightning
|
||||
"🎯", // Target
|
||||
]
|
||||
|
||||
export const AnimatedStatusIndicator: React.FC<AnimatedStatusIndicatorProps> = ({
|
||||
isStreaming,
|
||||
cost,
|
||||
cancelReason,
|
||||
apiRequestFailedMessage,
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { apiStatusConfig = {} } = useExtensionState()
|
||||
|
||||
// Configuration with defaults
|
||||
const config = useMemo(
|
||||
() => ({
|
||||
enabled: apiStatusConfig.enabled !== false,
|
||||
statusTexts:
|
||||
apiStatusConfig.customTexts && apiStatusConfig.customTexts.length > 0
|
||||
? apiStatusConfig.customTexts
|
||||
: DEFAULT_STATUS_TEXTS,
|
||||
emojisEnabled: apiStatusConfig.emojisEnabled === true,
|
||||
emojis:
|
||||
apiStatusConfig.customEmojis && apiStatusConfig.customEmojis.length > 0
|
||||
? apiStatusConfig.customEmojis
|
||||
: DEFAULT_EMOJIS,
|
||||
randomMode: apiStatusConfig.randomMode !== false,
|
||||
cycleInterval: apiStatusConfig.cycleInterval || 5000, // 5 seconds default
|
||||
}),
|
||||
[apiStatusConfig],
|
||||
)
|
||||
|
||||
const [currentTextIndex, setCurrentTextIndex] = useState(0)
|
||||
const [currentEmojiIndex, setCurrentEmojiIndex] = useState(0)
|
||||
|
||||
// Cycle through status texts and emojis
|
||||
useEffect(() => {
|
||||
if (!config.enabled || !isStreaming || !config.randomMode) return
|
||||
|
||||
const interval = setInterval(() => {
|
||||
setCurrentTextIndex((prev) => (prev + 1) % config.statusTexts.length)
|
||||
|
||||
if (config.emojisEnabled) {
|
||||
setCurrentEmojiIndex((prev) => (prev + 1) % config.emojis.length)
|
||||
}
|
||||
}, config.cycleInterval)
|
||||
|
||||
return () => clearInterval(interval)
|
||||
}, [config, isStreaming])
|
||||
|
||||
// Determine what text to show
|
||||
const statusText = useMemo(() => {
|
||||
if (cancelReason === "user_cancelled") {
|
||||
return t("chat:apiRequest.cancelled")
|
||||
}
|
||||
if (cancelReason) {
|
||||
return t("chat:apiRequest.streamingFailed")
|
||||
}
|
||||
if (cost !== null && cost !== undefined) {
|
||||
return t("chat:apiRequest.title")
|
||||
}
|
||||
if (apiRequestFailedMessage) {
|
||||
return t("chat:apiRequest.failed")
|
||||
}
|
||||
if (isStreaming && config.enabled) {
|
||||
return config.statusTexts[currentTextIndex]
|
||||
}
|
||||
return t("chat:apiRequest.streaming")
|
||||
}, [cancelReason, cost, apiRequestFailedMessage, isStreaming, config, currentTextIndex, t])
|
||||
|
||||
// Don't show animated indicator if request is complete or failed
|
||||
if (!isStreaming || cost !== null || cancelReason || apiRequestFailedMessage) {
|
||||
return null
|
||||
}
|
||||
|
||||
// If animation is disabled, return null (ChatRow will show default)
|
||||
if (!config.enabled) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
{config.emojisEnabled && (
|
||||
<span className="text-base animate-pulse-subtle">{config.emojis[currentEmojiIndex]}</span>
|
||||
)}
|
||||
<span
|
||||
className="text-vscode-foreground animate-pulse-subtle"
|
||||
style={{
|
||||
fontWeight: "bold",
|
||||
opacity: 0.9,
|
||||
}}>
|
||||
{statusText}
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -35,6 +35,7 @@ import { FollowUpSuggest } from "./FollowUpSuggest"
|
|||
import { BatchFilePermission } from "./BatchFilePermission"
|
||||
import { BatchDiffApproval } from "./BatchDiffApproval"
|
||||
import { ProgressIndicator } from "./ProgressIndicator"
|
||||
import { AnimatedStatusIndicator } from "./AnimatedStatusIndicator"
|
||||
import { Markdown } from "./Markdown"
|
||||
import { CommandExecution } from "./CommandExecution"
|
||||
import { CommandExecutionError } from "./CommandExecutionError"
|
||||
|
|
@ -252,6 +253,14 @@ export const ChatRowContent = ({
|
|||
<span style={{ color: normalColor, fontWeight: "bold" }}>{t("chat:apiRequest.title")}</span>
|
||||
) : apiRequestFailedMessage ? (
|
||||
<span style={{ color: errorColor, fontWeight: "bold" }}>{t("chat:apiRequest.failed")}</span>
|
||||
) : isStreaming ? (
|
||||
<AnimatedStatusIndicator
|
||||
isStreaming={true}
|
||||
cost={cost}
|
||||
cancelReason={apiReqCancelReason}
|
||||
apiRequestFailedMessage={apiRequestFailedMessage}
|
||||
streamingFailedMessage={apiReqStreamingFailedMessage}
|
||||
/>
|
||||
) : (
|
||||
<span style={{ color: normalColor, fontWeight: "bold" }}>{t("chat:apiRequest.streaming")}</span>
|
||||
),
|
||||
|
|
@ -267,7 +276,18 @@ export const ChatRowContent = ({
|
|||
default:
|
||||
return [null, null]
|
||||
}
|
||||
}, [type, isCommandExecuting, message, isMcpServerResponding, apiReqCancelReason, cost, apiRequestFailedMessage, t])
|
||||
}, [
|
||||
type,
|
||||
isCommandExecuting,
|
||||
message,
|
||||
isMcpServerResponding,
|
||||
apiReqCancelReason,
|
||||
cost,
|
||||
apiRequestFailedMessage,
|
||||
apiReqStreamingFailedMessage,
|
||||
isStreaming,
|
||||
t,
|
||||
])
|
||||
|
||||
const headerStyle: React.CSSProperties = {
|
||||
display: "flex",
|
||||
|
|
@ -968,6 +988,9 @@ export const ChatRowContent = ({
|
|||
/>
|
||||
)
|
||||
case "api_req_started":
|
||||
// Check if we should show the animated indicator
|
||||
const showAnimated = isLast && !cost && !apiReqCancelReason && !apiRequestFailedMessage
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
|
|
@ -987,8 +1010,23 @@ export const ChatRowContent = ({
|
|||
}}
|
||||
onClick={handleToggleExpand}>
|
||||
<div style={{ display: "flex", alignItems: "center", gap: "10px", flexGrow: 1 }}>
|
||||
{icon}
|
||||
{title}
|
||||
{showAnimated ? (
|
||||
<>
|
||||
<ProgressIndicator />
|
||||
<AnimatedStatusIndicator
|
||||
isStreaming={true}
|
||||
cost={cost}
|
||||
cancelReason={apiReqCancelReason}
|
||||
apiRequestFailedMessage={apiRequestFailedMessage}
|
||||
streamingFailedMessage={apiReqStreamingFailedMessage}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{icon}
|
||||
{title}
|
||||
</>
|
||||
)}
|
||||
<VSCodeBadge
|
||||
style={{ opacity: cost !== null && cost !== undefined && cost > 0 ? 1 : 0 }}>
|
||||
${Number(cost || 0)?.toFixed(4)}
|
||||
|
|
|
|||
221
webview-ui/src/components/settings/AnimatedStatusSettings.tsx
Normal file
221
webview-ui/src/components/settings/AnimatedStatusSettings.tsx
Normal file
|
|
@ -0,0 +1,221 @@
|
|||
import React, { useState, useEffect } from "react"
|
||||
import { VSCodeButton, VSCodeTextField, VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
|
||||
import { Plus, Trash2 } from "lucide-react"
|
||||
|
||||
interface AnimatedStatusSettingsProps {
|
||||
apiStatusConfig?: {
|
||||
enabled?: boolean
|
||||
customTexts?: string[]
|
||||
emojisEnabled?: boolean
|
||||
customEmojis?: string[]
|
||||
randomMode?: boolean
|
||||
cycleInterval?: number
|
||||
}
|
||||
setApiStatusConfig: (value: any) => void
|
||||
}
|
||||
|
||||
const DEFAULT_STATUS_TEXTS = ["Generating...", "Thinking...", "Working on it...", "Processing...", "Analyzing..."]
|
||||
|
||||
const DEFAULT_EMOJIS = [
|
||||
"🤔", // Thinking
|
||||
"🧠", // Brainstorming
|
||||
"⏳", // Loading
|
||||
"✨", // Magic
|
||||
"🔮", // Summoning
|
||||
"💭", // Thought bubble
|
||||
"⚡", // Lightning
|
||||
"🎯", // Target
|
||||
]
|
||||
|
||||
export const AnimatedStatusSettings: React.FC<AnimatedStatusSettingsProps> = ({
|
||||
apiStatusConfig = {},
|
||||
setApiStatusConfig,
|
||||
}) => {
|
||||
const [localConfig, setLocalConfig] = useState({
|
||||
enabled: apiStatusConfig.enabled !== false,
|
||||
customTexts: apiStatusConfig.customTexts || [],
|
||||
emojisEnabled: apiStatusConfig.emojisEnabled === true,
|
||||
customEmojis: apiStatusConfig.customEmojis || [],
|
||||
randomMode: apiStatusConfig.randomMode !== false,
|
||||
cycleInterval: apiStatusConfig.cycleInterval || 5000,
|
||||
})
|
||||
|
||||
const [newStatusText, setNewStatusText] = useState("")
|
||||
const [newEmoji, setNewEmoji] = useState("")
|
||||
|
||||
useEffect(() => {
|
||||
setApiStatusConfig(localConfig)
|
||||
}, [localConfig, setApiStatusConfig])
|
||||
|
||||
const addStatusText = () => {
|
||||
if (newStatusText.trim()) {
|
||||
setLocalConfig((prev) => ({
|
||||
...prev,
|
||||
customTexts: [...prev.customTexts, newStatusText.trim()],
|
||||
}))
|
||||
setNewStatusText("")
|
||||
}
|
||||
}
|
||||
|
||||
const removeStatusText = (index: number) => {
|
||||
setLocalConfig((prev) => ({
|
||||
...prev,
|
||||
customTexts: prev.customTexts.filter((_, i) => i !== index),
|
||||
}))
|
||||
}
|
||||
|
||||
const addEmoji = () => {
|
||||
if (newEmoji.trim()) {
|
||||
setLocalConfig((prev) => ({
|
||||
...prev,
|
||||
customEmojis: [...prev.customEmojis, newEmoji.trim()],
|
||||
}))
|
||||
setNewEmoji("")
|
||||
}
|
||||
}
|
||||
|
||||
const removeEmoji = (index: number) => {
|
||||
setLocalConfig((prev) => ({
|
||||
...prev,
|
||||
customEmojis: prev.customEmojis.filter((_, i) => i !== index),
|
||||
}))
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<VSCodeCheckbox
|
||||
checked={localConfig.enabled}
|
||||
onChange={(e: any) => setLocalConfig((prev) => ({ ...prev, enabled: e.target.checked }))}>
|
||||
Enable animated status indicator
|
||||
</VSCodeCheckbox>
|
||||
</div>
|
||||
|
||||
{localConfig.enabled && (
|
||||
<>
|
||||
<div className="ml-6 space-y-4">
|
||||
{/* Random Mode */}
|
||||
<div className="flex items-center gap-2">
|
||||
<VSCodeCheckbox
|
||||
checked={localConfig.randomMode}
|
||||
onChange={(e: any) =>
|
||||
setLocalConfig((prev) => ({ ...prev, randomMode: e.target.checked }))
|
||||
}>
|
||||
Cycle through status messages
|
||||
</VSCodeCheckbox>
|
||||
</div>
|
||||
|
||||
{localConfig.randomMode && (
|
||||
<div className="ml-6">
|
||||
<label className="block text-sm mb-1">Cycle interval (seconds)</label>
|
||||
<VSCodeTextField
|
||||
value={String(localConfig.cycleInterval / 1000)}
|
||||
onChange={(e: any) => {
|
||||
const seconds = parseFloat(e.target.value) || 5
|
||||
setLocalConfig((prev) => ({
|
||||
...prev,
|
||||
cycleInterval: seconds * 1000,
|
||||
}))
|
||||
}}
|
||||
style={{ width: "100px" }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Custom Status Texts */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-2">Status Messages</label>
|
||||
<div className="text-xs text-vscode-descriptionForeground mb-2">
|
||||
{localConfig.customTexts.length === 0
|
||||
? `Using default messages: ${DEFAULT_STATUS_TEXTS.join(", ")}`
|
||||
: "Custom messages:"}
|
||||
</div>
|
||||
|
||||
{localConfig.customTexts.map((text, index) => (
|
||||
<div key={index} className="flex items-center gap-2 mb-2">
|
||||
<span className="flex-1">{text}</span>
|
||||
<VSCodeButton appearance="icon" onClick={() => removeStatusText(index)}>
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
))}
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<VSCodeTextField
|
||||
value={newStatusText}
|
||||
onChange={(e: any) => setNewStatusText(e.target.value)}
|
||||
placeholder="Add custom status message..."
|
||||
onKeyDown={(e: any) => {
|
||||
if (e.key === "Enter") {
|
||||
addStatusText()
|
||||
}
|
||||
}}
|
||||
style={{ flex: 1 }}
|
||||
/>
|
||||
<VSCodeButton appearance="icon" onClick={addStatusText}>
|
||||
<Plus className="w-4 h-4" />
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Emoji Mode */}
|
||||
<div className="flex items-center gap-2">
|
||||
<VSCodeCheckbox
|
||||
checked={localConfig.emojisEnabled}
|
||||
onChange={(e: any) =>
|
||||
setLocalConfig((prev) => ({ ...prev, emojisEnabled: e.target.checked }))
|
||||
}>
|
||||
Show emoji with status
|
||||
</VSCodeCheckbox>
|
||||
</div>
|
||||
|
||||
{localConfig.emojisEnabled && (
|
||||
<div className="ml-6">
|
||||
<label className="block text-sm font-medium mb-2">Emojis</label>
|
||||
<div className="text-xs text-vscode-descriptionForeground mb-2">
|
||||
{localConfig.customEmojis.length === 0
|
||||
? `Using default emojis: ${DEFAULT_EMOJIS.join(" ")}`
|
||||
: "Custom emojis:"}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap gap-2 mb-2">
|
||||
{localConfig.customEmojis.map((emoji, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="flex items-center gap-1 px-2 py-1 bg-vscode-badge-background rounded">
|
||||
<span className="text-lg">{emoji}</span>
|
||||
<VSCodeButton
|
||||
appearance="icon"
|
||||
onClick={() => removeEmoji(index)}
|
||||
style={{ minWidth: "20px", height: "20px", padding: "2px" }}>
|
||||
<Trash2 className="w-3 h-3" />
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<VSCodeTextField
|
||||
value={newEmoji}
|
||||
onChange={(e: any) => setNewEmoji(e.target.value)}
|
||||
placeholder="Add emoji..."
|
||||
maxlength={2}
|
||||
onKeyDown={(e: any) => {
|
||||
if (e.key === "Enter") {
|
||||
addEmoji()
|
||||
}
|
||||
}}
|
||||
style={{ width: "100px" }}
|
||||
/>
|
||||
<VSCodeButton appearance="icon" onClick={addEmoji}>
|
||||
<Plus className="w-4 h-4" />
|
||||
</VSCodeButton>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -13,12 +13,15 @@ import { SectionHeader } from "./SectionHeader"
|
|||
import { Section } from "./Section"
|
||||
import { ExperimentalFeature } from "./ExperimentalFeature"
|
||||
import { ImageGenerationSettings } from "./ImageGenerationSettings"
|
||||
import { AnimatedStatusSettings } from "./AnimatedStatusSettings"
|
||||
|
||||
type ExperimentalSettingsProps = HTMLAttributes<HTMLDivElement> & {
|
||||
experiments: Experiments
|
||||
setExperimentEnabled: SetExperimentEnabled
|
||||
apiConfiguration?: any
|
||||
setApiConfigurationField?: any
|
||||
apiStatusConfig?: any
|
||||
setApiStatusConfig?: (value: any) => void
|
||||
}
|
||||
|
||||
export const ExperimentalSettings = ({
|
||||
|
|
@ -26,6 +29,8 @@ export const ExperimentalSettings = ({
|
|||
setExperimentEnabled,
|
||||
apiConfiguration,
|
||||
setApiConfigurationField,
|
||||
apiStatusConfig,
|
||||
setApiStatusConfig,
|
||||
className,
|
||||
...props
|
||||
}: ExperimentalSettingsProps) => {
|
||||
|
|
@ -41,6 +46,16 @@ export const ExperimentalSettings = ({
|
|||
</SectionHeader>
|
||||
|
||||
<Section>
|
||||
{/* Add Animated Status Settings at the top */}
|
||||
{setApiStatusConfig && (
|
||||
<div className="mb-4 pb-4 border-b border-vscode-panel-border">
|
||||
<AnimatedStatusSettings
|
||||
apiStatusConfig={apiStatusConfig}
|
||||
setApiStatusConfig={setApiStatusConfig}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{Object.entries(experimentConfigsMap)
|
||||
.filter(([key]) => key in EXPERIMENT_IDS)
|
||||
.map((config) => {
|
||||
|
|
|
|||
|
|
@ -343,6 +343,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration })
|
||||
vscode.postMessage({ type: "telemetrySetting", text: telemetrySetting })
|
||||
vscode.postMessage({ type: "profileThresholds", values: profileThresholds })
|
||||
vscode.postMessage({ type: "apiStatusConfig", values: cachedState.apiStatusConfig || {} })
|
||||
setChangeDetected(false)
|
||||
}
|
||||
}
|
||||
|
|
@ -723,6 +724,8 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone, t
|
|||
experiments={experiments}
|
||||
apiConfiguration={apiConfiguration}
|
||||
setApiConfigurationField={setApiConfigurationField}
|
||||
apiStatusConfig={cachedState.apiStatusConfig}
|
||||
setApiStatusConfig={(value) => setCachedStateField("apiStatusConfig", value)}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -48,6 +48,15 @@ export interface ExtensionStateContextType extends ExtensionState {
|
|||
setAlwaysAllowFollowupQuestions: (value: boolean) => void // Setter for the new property
|
||||
followupAutoApproveTimeoutMs: number | undefined // Timeout in ms for auto-approving follow-up questions
|
||||
setFollowupAutoApproveTimeoutMs: (value: number) => void // Setter for the timeout
|
||||
apiStatusConfig?: {
|
||||
enabled?: boolean
|
||||
customTexts?: string[]
|
||||
emojisEnabled?: boolean
|
||||
customEmojis?: string[]
|
||||
randomMode?: boolean
|
||||
cycleInterval?: number
|
||||
}
|
||||
setApiStatusConfig: (value: any) => void
|
||||
condensingApiConfigId?: string
|
||||
setCondensingApiConfigId: (value: string) => void
|
||||
customCondensingPrompt?: string
|
||||
|
|
@ -271,6 +280,14 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
global: {},
|
||||
})
|
||||
const [includeTaskHistoryInEnhance, setIncludeTaskHistoryInEnhance] = useState(true)
|
||||
const [apiStatusConfig, setApiStatusConfig] = useState<{
|
||||
enabled?: boolean
|
||||
customTexts?: string[]
|
||||
emojisEnabled?: boolean
|
||||
customEmojis?: string[]
|
||||
randomMode?: boolean
|
||||
cycleInterval?: number
|
||||
}>({})
|
||||
|
||||
const setListApiConfigMeta = useCallback(
|
||||
(value: ProviderSettingsEntry[]) => setState((prevState) => ({ ...prevState, listApiConfigMeta: value })),
|
||||
|
|
@ -308,6 +325,10 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
if ((newState as any).includeTaskHistoryInEnhance !== undefined) {
|
||||
setIncludeTaskHistoryInEnhance((newState as any).includeTaskHistoryInEnhance)
|
||||
}
|
||||
// Update apiStatusConfig if present in state message
|
||||
if ((newState as any).apiStatusConfig !== undefined) {
|
||||
setApiStatusConfig((newState as any).apiStatusConfig)
|
||||
}
|
||||
// Handle marketplace data if present in state message
|
||||
if (newState.marketplaceItems !== undefined) {
|
||||
setMarketplaceItems(newState.marketplaceItems)
|
||||
|
|
@ -411,6 +432,8 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
|
|||
marketplaceItems,
|
||||
marketplaceInstalledMetadata,
|
||||
profileThresholds: state.profileThresholds ?? {},
|
||||
apiStatusConfig,
|
||||
setApiStatusConfig,
|
||||
alwaysAllowFollowupQuestions,
|
||||
followupAutoApproveTimeoutMs,
|
||||
remoteControlEnabled: state.remoteControlEnabled ?? false,
|
||||
|
|
|
|||
|
|
@ -475,6 +475,20 @@ input[cmdk-input]:focus {
|
|||
animation: pulse 1.5s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse-subtle {
|
||||
0%,
|
||||
100% {
|
||||
opacity: 0.9;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.3;
|
||||
}
|
||||
}
|
||||
|
||||
.animate-pulse-subtle {
|
||||
animation: pulse-subtle 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
/* Transition utilities */
|
||||
.transition-all {
|
||||
transition-property: all;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue