diff --git a/.roo/rules/eslint-no-empty.md b/.roo/rules/eslint-no-empty.md new file mode 100644 index 0000000000..6cef399dea --- /dev/null +++ b/.roo/rules/eslint-no-empty.md @@ -0,0 +1,38 @@ +# Handling ESLint `no-empty` Rule + +Empty block statements (`{}`) are disallowed. All blocks MUST include a logging statement (e.g., `console.error`, `console.warn`, `console.log`, `console.debug`) to make their purpose explicit. + +This applies to `if`, `else`, `while`, `for`, `switch` cases, `try...catch` blocks, and function bodies. + +### Examples: + +```javascript +// Correct: Logging in blocks +if (condition) { + console.warn("Condition met, no specific action.") +} + +try { + criticalOperation() +} catch (error) { + // For unexpected errors: + console.error("Unexpected error in criticalOperation:", error) +} + +function foo() { + console.log("foo called, no operation.") +} +``` + +### Special Considerations: + +- **Intentional Error Suppression**: Use `console.debug` in `catch` blocks if an error is intentionally suppressed. + ```javascript + try { + operationThatMightBenignlyFail() + } catch (error) { + console.debug("Benign failure in operation, suppressed:", error) + } + ``` +- **Constructors**: Empty constructors should log, preferably with `console.debug`. +- **Comments**: Comments can supplement logs but do not replace the logging requirement. diff --git a/.roo/rules/eslint-no-implicit-coercion.md b/.roo/rules/eslint-no-implicit-coercion.md new file mode 100644 index 0000000000..8ed6ae004b --- /dev/null +++ b/.roo/rules/eslint-no-implicit-coercion.md @@ -0,0 +1,85 @@ +# Handling ESLint `no-implicit-coercion` Errors + +When ESLint's `no-implicit-coercion` rule flags an error, a value is being implicitly converted to a boolean, number, or string. While ESLint might suggest `Boolean(value)`, `Number(value)`, or `String(value)`, this project requires a different approach for boolean coercions. + +## Guideline: Explicit, Context-Aware Comparisons + +For boolean coercions (e.g., in `if` statements or ternaries), MUST NOT use `Boolean(value)`. Instead, extend conditional expressions to explicitly compare against the specific data type and value expected for that implementation context. + +- Leverage TypeScript type information to make these explicit comparisons precise. For example: + - Redundant checks for conditions already guaranteed by TypeScript type definitions (e.g., checking for `null` when a type is `SomeType | undefined`, or checking for `undefined` on a non-optional, initialized variable) MUST be omitted. + - If a string type is a union of specific non-empty literals (e.g., `"active" | "pending"`), `typeof variable === "string"` can be sufficient for its "truthiness" if all literals are inherently truthy. + - If types and initialization guarantee a variable is always defined and non-null (e.g., a non-optional class member), runtime checks for its mere existence can be redundant; the explicit boolean should reflect this guarantee. + +YOU MUST NEVER replace `if (myVar)` with `if (Boolean(myVar))` or `if (!!myVar)` with `if (Boolean(myVar))`. + +This rule's purpose is to encourage a thoughtful evaluation of what "truthy" or "falsy" means for the specific variable and logic. + +### Examples: + +## Incorrect (MUST NOT DO THIS): + +```typescript +// Implicit coercion (flagged by ESLint) +if (someStringOrNull) { + // ... +} + +// Explicit coercion using Boolean() (not the preferred fix here) +if (Boolean(someStringOrNull)) { + // ... +} +``` + +## Correct (Preferred Approach): + +- Checking for a non-empty string: + + ```typescript + if (typeof someStringOrNull === "string" && someStringOrNull != "") { + // ... + } + // Or, if an empty string is valid but null/undefined is not: + if (typeof someStringOrNull === "string") { + // ... + } + ``` + +- Checking against `null` or `undefined`: + + ```typescript + if (someValue !== null && someValue !== undefined) { + // ... + } + // Shorter (catches both null and undefined, mind other falsy values): + if (someValue != null) { + // ... + } + ``` + +- Checking if a variable is assigned (not `undefined`): + + ```typescript + if (someOptionalValue !== undefined) { + // ... + } + ``` + +- Checking if an array has elements: + ```typescript + if (Array.isArray(myArray) && myArray.length > 0) { + // ... + } + ``` + +### Rationale: + +Explicitly comparing types and values: + +1. Clarifies the code's intent. +2. Reduces ambiguity regarding how falsy values (`null`, `undefined`, `0`, `""`, `NaN`) are handled. +3. Helps avoid bugs from overly general truthiness checks when specific conditions were needed. +4. Be careful of regular expression evaluations. For example, `/foo (.*)bar/` will legitimately match an empty string, or it may not match at all. You MUST differentiate between `match === undefined` vs `typeof match === 'string' && match != ""` because falsy evaluation MUST NOT BE USED because it is usually invalid and certainly imprecise. +5. Helps in distinguishing the original intent behind a `!!variable` check: was it for general "truthiness" or mere "existence (not undefined/null)"? This complements the type-aware checks mentioned in the Guideline. + +Always consider the context: What does "truthy" or "falsy" mean for this variable in this logic? Write conditions reflecting that precise meaning. diff --git a/.roo/rules/eslint-no-unused-vars.md b/.roo/rules/eslint-no-unused-vars.md new file mode 100644 index 0000000000..a48d3e64a4 --- /dev/null +++ b/.roo/rules/eslint-no-unused-vars.md @@ -0,0 +1,63 @@ +# Handling `@typescript-eslint/no-unused-vars` + +If `@typescript-eslint/no-unused-vars` flags an error, a declaration is unused. + +## Guideline: Omit or Delete + +Unused declarations MUST be omitted or deleted. + +CRITICAL: Unused declarations MUST NOT be "fixed" by prefixing with an underscore (`_`). This is disallowed. Remove dead code, do not silence the linter. + +### Examples: + +#### Incorrect: + +```typescript +// error: 'unusedVar' is defined but never used. +function example(usedParam: string, unusedParam: number): void { + console.log(usedParam) + // Incorrect fix: + // const _unusedVar = 10; +} +``` + +```typescript +// error: 'error' is defined but never used. +try { + // ... +} catch (error) { + // 'error' is unused + // Incorrect fix: + // } catch (_error) { + console.error("An operation failed.") +} +``` + +#### Correct: + +```typescript +// 'unusedParam' removed if not needed by an interface/override. +function example(usedParam: string): void { + // 'unusedParam' removed + console.log(usedParam) + // 'unusedVar' is completely removed. +} +``` + +```typescript +// 'error' variable is removed from the catch block if not used. +try { + // ... +} catch { + // 'error' variable omitted entirely + console.error("An operation failed.") +} +``` + +### Rationale: + +- Clarity: Removing unused code improves readability. +- Bugs: Unused variables can indicate errors. +- No Workarounds: Prefixing with `_` hides issues. + +Code should be actively used. diff --git a/src/api/providers/__tests__/openai.spec.ts b/src/api/providers/__tests__/openai.spec.ts index 81c0b45e41..3b95ce729a 100644 --- a/src/api/providers/__tests__/openai.spec.ts +++ b/src/api/providers/__tests__/openai.spec.ts @@ -174,6 +174,7 @@ describe("OpenAiHandler", () => { const stream = reasoningHandler.createMessage(systemPrompt, messages) // Consume the stream to trigger the API call for await (const _chunk of stream) { + console.debug("Consuming stream for reasoning test") } // Assert the mockCreate was called with reasoning_effort expect(mockCreate).toHaveBeenCalled() @@ -191,6 +192,7 @@ describe("OpenAiHandler", () => { const stream = noReasoningHandler.createMessage(systemPrompt, messages) // Consume the stream to trigger the API call for await (const _chunk of stream) { + console.debug("Consuming stream for no-reasoning test") } // Assert the mockCreate was called without reasoning_effort expect(mockCreate).toHaveBeenCalled() diff --git a/src/api/providers/vscode-lm.ts b/src/api/providers/vscode-lm.ts index 6474371bee..f4b1d421ba 100644 --- a/src/api/providers/vscode-lm.ts +++ b/src/api/providers/vscode-lm.ts @@ -291,7 +291,7 @@ export class VsCodeLmHandler extends BaseProvider implements SingleCompletionHan if (!this.client) { console.debug("Roo Code : Getting client with options:", { vsCodeLmModelSelector: this.options.vsCodeLmModelSelector, - hasOptions: !!this.options, + hasOptions: true, // this.options is guaranteed to be an ApiHandlerOptions object selectorKeys: this.options.vsCodeLmModelSelector ? Object.keys(this.options.vsCodeLmModelSelector) : [], }) diff --git a/src/core/condense/index.ts b/src/core/condense/index.ts index 8a8b57bb0c..dc59a9d394 100644 --- a/src/core/condense/index.ts +++ b/src/core/condense/index.ts @@ -93,8 +93,8 @@ export async function summarizeConversation( TelemetryService.instance.captureContextCondensed( taskId, isAutomaticTrigger ?? false, - !!customCondensingPrompt?.trim(), - !!condensingApiHandler, + typeof customCondensingPrompt === "string" && customCondensingPrompt.trim() !== "", + condensingApiHandler !== undefined, ) const response: SummarizeResponse = { messages, cost: 0, summary: "" } diff --git a/src/core/config/ContextProxy.ts b/src/core/config/ContextProxy.ts index c4324fbb13..497002a81d 100644 --- a/src/core/config/ContextProxy.ts +++ b/src/core/config/ContextProxy.ts @@ -205,7 +205,7 @@ export class ContextProxy { await this.setValues({ ...PROVIDER_SETTINGS_KEYS.filter((key) => !isSecretStateKey(key)) - .filter((key) => !!this.stateCache[key]) + .filter((key) => this.stateCache[key] != null) .reduce((acc, key) => ({ ...acc, [key]: undefined }), {} as ProviderSettings), ...values, }) diff --git a/src/core/config/importExport.ts b/src/core/config/importExport.ts index 4830a5f987..ea2be6f0fc 100644 --- a/src/core/config/importExport.ts +++ b/src/core/config/importExport.ts @@ -117,5 +117,7 @@ export const exportSettings = async ({ providerSettingsManager, contextProxy }: const dirname = path.dirname(uri.fsPath) await fs.mkdir(dirname, { recursive: true }) await fs.writeFile(uri.fsPath, JSON.stringify({ providerProfiles, globalSettings }, null, 2), "utf-8") - } catch (e) {} + } catch (e) { + console.debug("Error exporting settings:", e) + } } diff --git a/src/core/tools/executeCommandTool.ts b/src/core/tools/executeCommandTool.ts index e38d3c74f6..41b0c3c164 100644 --- a/src/core/tools/executeCommandTool.ts +++ b/src/core/tools/executeCommandTool.ts @@ -171,7 +171,9 @@ export async function executeCommand( message = { text, images } process.continue() } - } catch (_error) {} + } catch (_error) { + console.debug("Error in onWillRespond:", _error) + } }, onCompleted: (output: string | undefined) => { result = Terminal.compressTerminalOutput(output ?? "", terminalOutputLineLimit) @@ -197,7 +199,12 @@ export async function executeCommand( } } - const terminal = await TerminalRegistry.getOrCreateTerminal(workingDir, !!customCwd, cline.taskId, terminalProvider) + const terminal = await TerminalRegistry.getOrCreateTerminal( + workingDir, + typeof customCwd === "string" && customCwd !== "", + cline.taskId, + terminalProvider, + ) if (terminal instanceof Terminal) { terminal.terminal.show(true) diff --git a/src/core/tools/writeToFileTool.ts b/src/core/tools/writeToFileTool.ts index 63191acb7e..83543919b7 100644 --- a/src/core/tools/writeToFileTool.ts +++ b/src/core/tools/writeToFileTool.ts @@ -123,7 +123,7 @@ export async function writeToFileTool( const isNewFile = !fileExists // Check if diffStrategy is enabled - const diffStrategyEnabled = !!cline.diffStrategy + const diffStrategyEnabled = cline.diffStrategy !== undefined // Use more specific error message for line_count that provides guidance based on the situation await cline.say( diff --git a/src/core/webview/ClineProvider.ts b/src/core/webview/ClineProvider.ts index 172685affd..ccf26d41cd 100644 --- a/src/core/webview/ClineProvider.ts +++ b/src/core/webview/ClineProvider.ts @@ -825,7 +825,7 @@ export class ClineProvider } public hasProviderProfileEntry(name: string): boolean { - return !!this.getProviderProfileEntry(name) + return this.getProviderProfileEntry(name) !== undefined } async upsertProviderProfile( @@ -1656,7 +1656,7 @@ export class ClineProvider apiProvider: apiConfiguration?.apiProvider, modelId: task?.api?.getModel().id, diffStrategy: task?.diffStrategy?.getName(), - isSubtask: task ? !!task.parentTask : undefined, + isSubtask: task ? task.parentTask !== undefined : undefined, } } } diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index 659d60f31a..e4087708ac 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -629,7 +629,7 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We // Send the result back to the webview await provider.postMessageToWebview({ type: "browserConnectionResult", - success: !!chromeHostUrl, + success: typeof chromeHostUrl === "string" && chromeHostUrl !== "", text: `Auto-discovered and tested connection to Chrome: ${chromeHostUrl}`, values: { endpoint: chromeHostUrl }, }) @@ -1008,7 +1008,10 @@ export const webviewMessageHandler = async (provider: ClineProvider, message: We // Try to get enhancement config first, fall back to current config. let configToUse: ProviderSettings = apiConfiguration - if (enhancementApiConfigId && !!listApiConfigMeta.find(({ id }) => id === enhancementApiConfigId)) { + if ( + enhancementApiConfigId && + listApiConfigMeta.find(({ id }) => id === enhancementApiConfigId) !== undefined + ) { const { name: _, ...providerSettings } = await provider.providerSettingsManager.getProfile({ id: enhancementApiConfigId, }) diff --git a/src/eslint.config.mjs b/src/eslint.config.mjs index d0813406d9..4f70db5753 100644 --- a/src/eslint.config.mjs +++ b/src/eslint.config.mjs @@ -8,9 +8,12 @@ export default [ // TODO: These should be fixed and the rules re-enabled. "no-regex-spaces": "off", "no-useless-escape": "off", - "no-empty": "off", "prefer-const": "off", + "no-empty": "error", + "no-implicit-coercion": "error", + "no-useless-catch": "error", + "@typescript-eslint/no-unused-vars": "off", "@typescript-eslint/no-explicit-any": "off", "@typescript-eslint/no-require-imports": "off", diff --git a/src/integrations/misc/open-file.ts b/src/integrations/misc/open-file.ts index 9318e23766..c542738cb1 100644 --- a/src/integrations/misc/open-file.ts +++ b/src/integrations/misc/open-file.ts @@ -138,7 +138,9 @@ export async function openFile(filePath: string, options: OpenFileOptions = {}) break } } - } catch {} // not essential, sometimes tab operations fail + } catch (e) { + console.debug("Error processing tab operations:", e) + } // not essential, sometimes tab operations fail const document = await vscode.workspace.openTextDocument(uriToProcess) const selection = diff --git a/src/integrations/terminal/ExecaTerminalProcess.ts b/src/integrations/terminal/ExecaTerminalProcess.ts index 2b9b97d10a..1f9478e573 100644 --- a/src/integrations/terminal/ExecaTerminalProcess.ts +++ b/src/integrations/terminal/ExecaTerminalProcess.ts @@ -76,7 +76,9 @@ export class ExecaTerminalProcess extends BaseTerminalProcess { timeoutId = setTimeout(() => { try { subprocess.kill("SIGKILL") - } catch (e) {} + } catch (e) { + console.debug("Error killing subprocess:", e) + } resolve() }, 5_000) diff --git a/src/services/checkpoints/ShadowCheckpointService.ts b/src/services/checkpoints/ShadowCheckpointService.ts index 8ec82f77ec..c1b39379cc 100644 --- a/src/services/checkpoints/ShadowCheckpointService.ts +++ b/src/services/checkpoints/ShadowCheckpointService.ts @@ -36,7 +36,7 @@ export abstract class ShadowCheckpointService extends EventEmitter { } public get isInitialized() { - return !!this.git + return this.git !== undefined } constructor(taskId: string, checkpointsDir: string, workspaceDir: string, log: (message: string) => void) { diff --git a/src/services/checkpoints/excludes.ts b/src/services/checkpoints/excludes.ts index 52469fd620..4e49ebdb32 100644 --- a/src/services/checkpoints/excludes.ts +++ b/src/services/checkpoints/excludes.ts @@ -193,7 +193,9 @@ const getLfsPatterns = async (workspacePath: string) => { .filter((line) => line.includes("filter=lfs")) .map((line) => line.split(" ")[0].trim()) } - } catch (error) {} + } catch (error) { + console.debug("Error getting LFS tracked files:", error) + } return [] } diff --git a/src/services/code-index/config-manager.ts b/src/services/code-index/config-manager.ts index 730f43e3c5..7b31aa983a 100644 --- a/src/services/code-index/config-manager.ts +++ b/src/services/code-index/config-manager.ts @@ -124,13 +124,18 @@ export class CodeIndexConfigManager { if (this.embedderProvider === "openai") { const openAiKey = this.openAiOptions?.openAiNativeApiKey const qdrantUrl = this.qdrantUrl - const isConfigured = !!(openAiKey && qdrantUrl) + const isConfigured = + typeof openAiKey === "string" && openAiKey !== "" && typeof qdrantUrl === "string" && qdrantUrl !== "" return isConfigured } else if (this.embedderProvider === "ollama") { // Ollama model ID has a default, so only base URL is strictly required for config const ollamaBaseUrl = this.ollamaOptions?.ollamaBaseUrl const qdrantUrl = this.qdrantUrl - const isConfigured = !!(ollamaBaseUrl && qdrantUrl) + const isConfigured = + typeof ollamaBaseUrl === "string" && + ollamaBaseUrl !== "" && + typeof qdrantUrl === "string" && + qdrantUrl !== "" return isConfigured } return false // Should not happen if embedderProvider is always set correctly diff --git a/src/shared/api.ts b/src/shared/api.ts index 8ad8828658..f6f05fc713 100644 --- a/src/shared/api.ts +++ b/src/shared/api.ts @@ -34,7 +34,9 @@ export const shouldUseReasoningBudget = ({ }: { model: ModelInfo settings?: ProviderSettings -}): boolean => !!model.requiredReasoningBudget || (!!model.supportsReasoningBudget && !!settings?.enableReasoningEffort) +}): boolean => + model.requiredReasoningBudget === true || + (model.supportsReasoningBudget === true && settings?.enableReasoningEffort === true) export const shouldUseReasoningEffort = ({ model, @@ -42,7 +44,14 @@ export const shouldUseReasoningEffort = ({ }: { model: ModelInfo settings?: ProviderSettings -}): boolean => (!!model.supportsReasoningEffort && !!settings?.reasoningEffort) || !!model.reasoningEffort +}): boolean => + (model.supportsReasoningEffort === true && + settings?.reasoningEffort !== undefined && + (typeof settings.reasoningEffort === "string" || // String literals are inherently non-empty + (typeof settings.reasoningEffort === "number" && settings.reasoningEffort !== 0))) || + (model.reasoningEffort !== undefined && + (typeof model.reasoningEffort === "string" || // String literals are inherently non-empty + (typeof model.reasoningEffort === "number" && model.reasoningEffort !== 0))) export const DEFAULT_HYBRID_REASONING_MODEL_MAX_TOKENS = 16_384 export const DEFAULT_HYBRID_REASONING_MODEL_THINKING_TOKENS = 8_192 diff --git a/src/shared/combineApiRequests.ts b/src/shared/combineApiRequests.ts index 20ba6bb6aa..dbfc24ea77 100644 --- a/src/shared/combineApiRequests.ts +++ b/src/shared/combineApiRequests.ts @@ -68,13 +68,17 @@ export function combineApiRequests(messages: ClineMessage[]): ClineMessage[] { if (startMessage.text) { startData = JSON.parse(startMessage.text) } - } catch (e) {} + } catch (e) { + console.debug("Error parsing startMessage text:", e) + } try { if (message.text) { finishData = JSON.parse(message.text) } - } catch (e) {} + } catch (e) { + console.debug("Error parsing message text:", e) + } result[startIndex] = { ...startMessage, text: JSON.stringify({ ...startData, ...finishData }) } } diff --git a/src/shared/modes.ts b/src/shared/modes.ts index c735118f66..ad8946ecf7 100644 --- a/src/shared/modes.ts +++ b/src/shared/modes.ts @@ -154,7 +154,7 @@ export function getAllModes(customModes?: ModeConfig[]): ModeConfig[] { // Check if a mode is custom or an override export function isCustomMode(slug: string, customModes?: ModeConfig[]): boolean { - return !!customModes?.some((mode) => mode.slug === slug) + return Array.isArray(customModes) && customModes.some((mode) => mode.slug === slug) } /** diff --git a/src/utils/tts.ts b/src/utils/tts.ts index b544960571..eba572ceb3 100644 --- a/src/utils/tts.ts +++ b/src/utils/tts.ts @@ -32,7 +32,9 @@ export const playTts = async (message: string, options: PlayTtsOptions = {}) => try { queue.push({ message, options }) await processQueue() - } catch (error) {} + } catch (error) { + console.debug("Error processing TTS queue:", error) + } } export const stopTts = () => { diff --git a/webview-ui/eslint.config.mjs b/webview-ui/eslint.config.mjs index db76f49211..035a8e6a2e 100644 --- a/webview-ui/eslint.config.mjs +++ b/webview-ui/eslint.config.mjs @@ -18,6 +18,9 @@ export default [ "@typescript-eslint/no-explicit-any": "off", "react/prop-types": "off", "react/display-name": "off", + "no-empty": "error", + "no-implicit-coercion": "error", + "no-useless-catch": "error", }, }, { diff --git a/webview-ui/src/components/chat/AutoApproveMenu.tsx b/webview-ui/src/components/chat/AutoApproveMenu.tsx index ac02d6b8c4..f1907100e4 100644 --- a/webview-ui/src/components/chat/AutoApproveMenu.tsx +++ b/webview-ui/src/components/chat/AutoApproveMenu.tsx @@ -108,7 +108,7 @@ const AutoApproveMenu = ({ style }: AutoApproveMenuProps) => { ) const enabledActionsList = Object.entries(toggles) - .filter(([_key, value]) => !!value) + .filter(([_key, value]) => value === true) .map(([key]) => t(autoApproveSettingsConfig[key as AutoApproveSetting].labelKey)) .join(", ") diff --git a/webview-ui/src/components/chat/ChatView.tsx b/webview-ui/src/components/chat/ChatView.tsx index d114701e18..f1c876d8bb 100644 --- a/webview-ui/src/components/chat/ChatView.tsx +++ b/webview-ui/src/components/chat/ChatView.tsx @@ -159,7 +159,9 @@ const ChatViewComponent: React.ForwardRefRenderFunction !!apiConfiguration && !ProfileValidator.isProfileAllowed(apiConfiguration, organizationAllowList), + () => + apiConfiguration !== undefined && + !ProfileValidator.isProfileAllowed(apiConfiguration, organizationAllowList), [apiConfiguration, organizationAllowList], ) @@ -429,7 +431,7 @@ const ChatViewComponent: React.ForwardRefRenderFunction { disabled={buttonsDisabled} onClick={() => vscode.postMessage({ type: "exportCurrentTask" })} /> - {!!item?.size && item.size > 0 && ( + {typeof item?.size === "number" && item.size > 0 && ( <>
{condenseButton} - {!!totalCost && ${totalCost.toFixed(2)}} + {totalCost > 0 && ${totalCost.toFixed(2)}}
)} {/* Expanded state: Show task text and images */} @@ -204,7 +204,7 @@ const TaskHeader = ({ )} - {!!totalCost && ( + {totalCost > 0 && (
{t("chat:task.apiCost")} diff --git a/webview-ui/src/components/history/TaskItemFooter.tsx b/webview-ui/src/components/history/TaskItemFooter.tsx index b3e6e56371..8682f84fad 100644 --- a/webview-ui/src/components/history/TaskItemFooter.tsx +++ b/webview-ui/src/components/history/TaskItemFooter.tsx @@ -34,7 +34,7 @@ const TaskItemFooter: React.FC = ({ item, variant, isSelect {isCompact ? ( <> {/* Compact Cache */} - {!!item.cacheWrites && ( + {typeof item.cacheWrites === "number" && item.cacheWrites > 0 && ( {formatLargeNumber(item.cacheWrites || 0)} @@ -42,7 +42,7 @@ const TaskItemFooter: React.FC = ({ item, variant, isSelect {formatLargeNumber(item.cacheReads || 0)} )} - + {/* Compact Tokens */} {(item.tokensIn || item.tokensOut) && ( <> @@ -55,7 +55,7 @@ const TaskItemFooter: React.FC = ({ item, variant, isSelect )} {/* Compact Cost */} - {!!item.totalCost && ( + {item.totalCost > 0 && ( {"$" + item.totalCost.toFixed(2)} @@ -66,7 +66,7 @@ const TaskItemFooter: React.FC = ({ item, variant, isSelect <>
{/* Cache Info */} - {!!item.cacheWrites && ( + {typeof item.cacheWrites === "number" && item.cacheWrites > 0 && (
{t("history:cacheLabel")} @@ -74,12 +74,15 @@ const TaskItemFooter: React.FC = ({ item, variant, isSelect {formatLargeNumber(item.cacheWrites || 0)} - + {formatLargeNumber(item.cacheReads || 0)}
)} - + {/* Full Tokens */} {(item.tokensIn || item.tokensOut) && (
@@ -95,7 +98,7 @@ const TaskItemFooter: React.FC = ({ item, variant, isSelect
)} {/* Full Cost */} - {!!item.totalCost && ( + {item.totalCost > 0 && (
{t("history:apiCostLabel")} {"$" + item.totalCost.toFixed(4)} diff --git a/webview-ui/src/components/settings/ApiConfigManager.tsx b/webview-ui/src/components/settings/ApiConfigManager.tsx index ddcfae49b3..7db5c767ab 100644 --- a/webview-ui/src/components/settings/ApiConfigManager.tsx +++ b/webview-ui/src/components/settings/ApiConfigManager.tsx @@ -72,7 +72,10 @@ const ApiConfigManager = ({ } // If provider allows all models, profile is valid - return !!providerConfig.allowAll || !!(providerConfig.models && providerConfig.models.length > 0) + return ( + providerConfig.allowAll === true || + (Array.isArray(providerConfig.models) && providerConfig.models.length > 0) + ) } const validateName = (name: string, isNewProfile: boolean): string | null => { diff --git a/webview-ui/src/components/settings/AutoApproveToggle.tsx b/webview-ui/src/components/settings/AutoApproveToggle.tsx index ffad47e2ac..27a41a623a 100644 --- a/webview-ui/src/components/settings/AutoApproveToggle.tsx +++ b/webview-ui/src/components/settings/AutoApproveToggle.tsx @@ -106,7 +106,7 @@ export const AutoApproveToggle = ({ onToggle, ...props }: AutoApproveToggleProps onClick={() => onToggle(key, !props[key])} title={t(descriptionKey || "")} aria-label={t(labelKey)} - aria-pressed={!!props[key]} + aria-pressed={props[key] === true} data-testid={testId} className={cn(" aspect-square h-[80px]", !props[key] && "opacity-50")}> diff --git a/webview-ui/src/components/settings/ExperimentalSettings.tsx b/webview-ui/src/components/settings/ExperimentalSettings.tsx index 5525a842b2..d8bc6691d5 100644 --- a/webview-ui/src/components/settings/ExperimentalSettings.tsx +++ b/webview-ui/src/components/settings/ExperimentalSettings.tsx @@ -78,7 +78,10 @@ export const ExperimentalSettings = ({ experimentKey={config[0]} enabled={experiments[EXPERIMENT_IDS[config[0] as keyof typeof EXPERIMENT_IDS]] ?? false} onChange={(enabled) => - setExperimentEnabled(EXPERIMENT_IDS[config[0] as keyof typeof EXPERIMENT_IDS], enabled) + setExperimentEnabled( + EXPERIMENT_IDS[config[0] as keyof typeof EXPERIMENT_IDS], + enabled, + ) } /> ) diff --git a/webview-ui/src/components/settings/ThinkingBudget.tsx b/webview-ui/src/components/settings/ThinkingBudget.tsx index 456e0be17a..ddb66f1d29 100644 --- a/webview-ui/src/components/settings/ThinkingBudget.tsx +++ b/webview-ui/src/components/settings/ThinkingBudget.tsx @@ -17,9 +17,9 @@ interface ThinkingBudgetProps { export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, modelInfo }: ThinkingBudgetProps) => { const { t } = useAppTranslation() - const isReasoningBudgetSupported = !!modelInfo && modelInfo.supportsReasoningBudget - const isReasoningBudgetRequired = !!modelInfo && modelInfo.requiredReasoningBudget - const isReasoningEffortSupported = !!modelInfo && modelInfo.supportsReasoningEffort + const isReasoningBudgetSupported = modelInfo !== undefined && modelInfo.supportsReasoningBudget === true + const isReasoningBudgetRequired = modelInfo !== undefined && modelInfo.requiredReasoningBudget === true + const isReasoningEffortSupported = modelInfo !== undefined && modelInfo.supportsReasoningEffort === true const enableReasoningEffort = apiConfiguration.enableReasoningEffort const customMaxOutputTokens = apiConfiguration.modelMaxTokens || DEFAULT_HYBRID_REASONING_MODEL_MAX_TOKENS @@ -45,7 +45,9 @@ export const ThinkingBudget = ({ apiConfiguration, setApiConfigurationField, mod return null } - return isReasoningBudgetSupported && !!modelInfo.maxTokens ? ( + return isReasoningBudgetSupported && + typeof modelInfo.maxTokens === "number" && // modelInfo is guaranteed to be defined here + modelInfo.maxTokens > 0 ? ( <> {!isReasoningBudgetRequired && (
diff --git a/webview-ui/src/components/settings/providers/Anthropic.tsx b/webview-ui/src/components/settings/providers/Anthropic.tsx index f340e73f72..9830b3b112 100644 --- a/webview-ui/src/components/settings/providers/Anthropic.tsx +++ b/webview-ui/src/components/settings/providers/Anthropic.tsx @@ -17,7 +17,9 @@ type AnthropicProps = { export const Anthropic = ({ apiConfiguration, setApiConfigurationField }: AnthropicProps) => { const { t } = useAppTranslation() - const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState(!!apiConfiguration?.anthropicBaseUrl) + const [anthropicBaseUrlSelected, setAnthropicBaseUrlSelected] = useState( + typeof apiConfiguration?.anthropicBaseUrl === "string" && apiConfiguration.anthropicBaseUrl !== "", + ) const handleInputChange = useCallback( ( diff --git a/webview-ui/src/components/settings/providers/Bedrock.tsx b/webview-ui/src/components/settings/providers/Bedrock.tsx index eb8ca94258..7fe124430d 100644 --- a/webview-ui/src/components/settings/providers/Bedrock.tsx +++ b/webview-ui/src/components/settings/providers/Bedrock.tsx @@ -17,11 +17,11 @@ type BedrockProps = { export const Bedrock = ({ apiConfiguration, setApiConfigurationField, selectedModelInfo }: BedrockProps) => { const { t } = useAppTranslation() - const [awsEndpointSelected, setAwsEndpointSelected] = useState(!!apiConfiguration?.awsBedrockEndpointEnabled) + const [awsEndpointSelected, setAwsEndpointSelected] = useState(apiConfiguration?.awsBedrockEndpointEnabled === true) // Update the endpoint enabled state when the configuration changes useEffect(() => { - setAwsEndpointSelected(!!apiConfiguration?.awsBedrockEndpointEnabled) + setAwsEndpointSelected(apiConfiguration?.awsBedrockEndpointEnabled === true) }, [apiConfiguration?.awsBedrockEndpointEnabled]) const handleInputChange = useCallback( diff --git a/webview-ui/src/components/settings/providers/Gemini.tsx b/webview-ui/src/components/settings/providers/Gemini.tsx index 21056f12d5..86ed562dca 100644 --- a/webview-ui/src/components/settings/providers/Gemini.tsx +++ b/webview-ui/src/components/settings/providers/Gemini.tsx @@ -18,7 +18,7 @@ export const Gemini = ({ apiConfiguration, setApiConfigurationField }: GeminiPro const { t } = useAppTranslation() const [googleGeminiBaseUrlSelected, setGoogleGeminiBaseUrlSelected] = useState( - !!apiConfiguration?.googleGeminiBaseUrl, + typeof apiConfiguration?.googleGeminiBaseUrl === "string" && apiConfiguration.googleGeminiBaseUrl !== "", ) const handleInputChange = useCallback( diff --git a/webview-ui/src/components/settings/providers/OpenAI.tsx b/webview-ui/src/components/settings/providers/OpenAI.tsx index e2f7857fe0..905adb71eb 100644 --- a/webview-ui/src/components/settings/providers/OpenAI.tsx +++ b/webview-ui/src/components/settings/providers/OpenAI.tsx @@ -18,7 +18,7 @@ export const OpenAI = ({ apiConfiguration, setApiConfigurationField }: OpenAIPro const { t } = useAppTranslation() const [openAiNativeBaseUrlSelected, setOpenAiNativeBaseUrlSelected] = useState( - !!apiConfiguration?.openAiNativeBaseUrl, + typeof apiConfiguration?.openAiNativeBaseUrl === "string" && apiConfiguration.openAiNativeBaseUrl !== "", ) const handleInputChange = useCallback( diff --git a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx index 43fea540c3..a73a56abd6 100644 --- a/webview-ui/src/components/settings/providers/OpenAICompatible.tsx +++ b/webview-ui/src/components/settings/providers/OpenAICompatible.tsx @@ -36,8 +36,12 @@ export const OpenAICompatible = ({ }: OpenAICompatibleProps) => { const { t } = useAppTranslation() - const [azureApiVersionSelected, setAzureApiVersionSelected] = useState(!!apiConfiguration?.azureApiVersion) - const [openAiLegacyFormatSelected, setOpenAiLegacyFormatSelected] = useState(!!apiConfiguration?.openAiLegacyFormat) + const [azureApiVersionSelected, setAzureApiVersionSelected] = useState( + typeof apiConfiguration?.azureApiVersion === "string" && apiConfiguration.azureApiVersion !== "", + ) + const [openAiLegacyFormatSelected, setOpenAiLegacyFormatSelected] = useState( + apiConfiguration?.openAiLegacyFormat === true, + ) const [openAiModels, setOpenAiModels] = useState | null>(null) @@ -244,7 +248,7 @@ export const OpenAICompatible = ({ }}> {t("settings:providers.setReasoningLevel")} - {!!apiConfiguration.enableReasoningEffort && ( + {apiConfiguration.enableReasoningEffort === true && ( { const { t } = useAppTranslation() - const [openRouterBaseUrlSelected, setOpenRouterBaseUrlSelected] = useState(!!apiConfiguration?.openRouterBaseUrl) + const [openRouterBaseUrlSelected, setOpenRouterBaseUrlSelected] = useState( + typeof apiConfiguration?.openRouterBaseUrl === "string" && apiConfiguration.openRouterBaseUrl !== "", + ) const handleInputChange = useCallback( ( @@ -58,8 +60,10 @@ export const OpenRouter = ({ const { data: openRouterModelProviders } = useOpenRouterModelProviders(apiConfiguration?.openRouterModelId, { enabled: - !!apiConfiguration?.openRouterModelId && - routerModels?.openrouter && + typeof apiConfiguration?.openRouterModelId === "string" && + apiConfiguration.openRouterModelId !== "" && + routerModels?.openrouter !== undefined && + routerModels?.openrouter !== null && Object.keys(routerModels.openrouter).length > 1 && apiConfiguration.openRouterModelId in routerModels.openrouter, }) diff --git a/webview-ui/src/components/settings/providers/__tests__/Bedrock.test.tsx b/webview-ui/src/components/settings/providers/__tests__/Bedrock.test.tsx index c1f177ac7b..556de4ded1 100644 --- a/webview-ui/src/components/settings/providers/__tests__/Bedrock.test.tsx +++ b/webview-ui/src/components/settings/providers/__tests__/Bedrock.test.tsx @@ -353,7 +353,9 @@ describe("Bedrock Component", () => { ) // Verify checkbox is checked and endpoint is visible - expect(screen.getByTestId("checkbox-input-settings:providers.awsbedrockvpc.usecustomvpcendpoint")).toBeChecked() + expect( + screen.getByTestId("checkbox-input-settings:providers.awsbedrockvpc.usecustomvpcendpoint"), + ).toBeChecked() expect(screen.getByTestId("vpc-endpoint-input")).toBeInTheDocument() expect(screen.getByTestId("vpc-endpoint-input")).toHaveValue("https://custom-endpoint.aws.com") @@ -374,7 +376,9 @@ describe("Bedrock Component", () => { ) // Verify checkbox is unchecked and endpoint is not visible - expect(screen.getByTestId("checkbox-input-settings:providers.awsbedrockvpc.usecustomvpcendpoint")).not.toBeChecked() + expect( + screen.getByTestId("checkbox-input-settings:providers.awsbedrockvpc.usecustomvpcendpoint"), + ).not.toBeChecked() expect(screen.queryByTestId("vpc-endpoint-input")).not.toBeInTheDocument() }) @@ -394,7 +398,9 @@ describe("Bedrock Component", () => { ) // Verify initial state - expect(screen.getByTestId("checkbox-input-settings:providers.awsbedrockvpc.usecustomvpcendpoint")).not.toBeChecked() + expect( + screen.getByTestId("checkbox-input-settings:providers.awsbedrockvpc.usecustomvpcendpoint"), + ).not.toBeChecked() expect(screen.queryByTestId("vpc-endpoint-input")).not.toBeInTheDocument() // Update with new configuration @@ -412,7 +418,9 @@ describe("Bedrock Component", () => { ) // Verify updated state - expect(screen.getByTestId("checkbox-input-settings:providers.awsbedrockvpc.usecustomvpcendpoint")).toBeChecked() + expect( + screen.getByTestId("checkbox-input-settings:providers.awsbedrockvpc.usecustomvpcendpoint"), + ).toBeChecked() expect(screen.getByTestId("vpc-endpoint-input")).toBeInTheDocument() expect(screen.getByTestId("vpc-endpoint-input")).toHaveValue("https://updated-endpoint.aws.com") }) diff --git a/webview-ui/src/components/ui/chat/ChatInput.tsx b/webview-ui/src/components/ui/chat/ChatInput.tsx index 8bec35b842..ea98cda713 100644 --- a/webview-ui/src/components/ui/chat/ChatInput.tsx +++ b/webview-ui/src/components/ui/chat/ChatInput.tsx @@ -80,7 +80,7 @@ function ChatInputField({ placeholder = "Chat" }: ChatInputFieldProps) { function ChatInputSubmit() { const { isLoading, stop } = useChatUI() const { isDisabled } = useChatInput() - const isStoppable = isLoading && !!stop + const isStoppable = isLoading && typeof stop === "function" return (
diff --git a/webview-ui/src/components/ui/chat/ChatMessages.tsx b/webview-ui/src/components/ui/chat/ChatMessages.tsx index bcaffd2397..61d50e5ffa 100644 --- a/webview-ui/src/components/ui/chat/ChatMessages.tsx +++ b/webview-ui/src/components/ui/chat/ChatMessages.tsx @@ -29,7 +29,9 @@ export function ChatMessages() { key={index} message={message} isHeaderVisible={ - !!message.annotations?.length || index === 0 || messages[index - 1].role !== message.role + (Array.isArray(message.annotations) && message.annotations.length > 0) || + index === 0 || + messages[index - 1].role !== message.role } isLast={index === messageCount - 1} isLoading={isLoading} diff --git a/webview-ui/src/components/ui/hooks/useOpenRouterKeyInfo.ts b/webview-ui/src/components/ui/hooks/useOpenRouterKeyInfo.ts index 44b7aaade5..373d5392a6 100644 --- a/webview-ui/src/components/ui/hooks/useOpenRouterKeyInfo.ts +++ b/webview-ui/src/components/ui/hooks/useOpenRouterKeyInfo.ts @@ -53,7 +53,7 @@ export const useOpenRouterKeyInfo = (apiKey?: string, baseUrl?: string, options? queryKey: ["openrouter-key-info", apiKey, baseUrl], queryFn: () => getOpenRouterKeyInfo(apiKey, baseUrl), staleTime: 30 * 1000, // 30 seconds - enabled: !!apiKey, + enabled: typeof apiKey === "string" && apiKey !== "", ...options, }) } diff --git a/webview-ui/src/components/ui/hooks/useRequestyKeyInfo.ts b/webview-ui/src/components/ui/hooks/useRequestyKeyInfo.ts index e44aa5fcd1..014a694664 100644 --- a/webview-ui/src/components/ui/hooks/useRequestyKeyInfo.ts +++ b/webview-ui/src/components/ui/hooks/useRequestyKeyInfo.ts @@ -44,7 +44,7 @@ export const useRequestyKeyInfo = (apiKey?: string, options?: UseRequestyKeyInfo queryKey: ["requesty-key-info", apiKey], queryFn: () => getRequestyKeyInfo(apiKey), staleTime: 30 * 1000, // 30 seconds - enabled: !!apiKey, + enabled: typeof apiKey === "string" && apiKey !== "", ...options, }) }