diff --git a/Makefile b/Makefile index dc1575362..ccb698f0e 100644 --- a/Makefile +++ b/Makefile @@ -155,6 +155,17 @@ typecheck: node_modules/.installed src/version.ts ## Run TypeScript type checkin "$(TSGO) --noEmit" \ "$(TSGO) --noEmit -p tsconfig.main.json" +check-deadcode: node_modules/.installed ## Check for potential dead code (manual only, not in static-check) + @echo "Checking for potential dead code with ts-prune..." + @echo "(Note: Some unused exports are legitimate - types, public APIs, entry points, etc.)" + @echo "" + @bun x ts-prune -i '(test|spec|mock|bench|debug|storybook)' \ + | grep -v "used in module" \ + | grep -v "src/App.tsx.*default" \ + | grep -v "src/types/" \ + | grep -v "telemetry/index.ts" \ + || echo "✓ No obvious dead code found" + ## Testing test-integration: node_modules/.installed ## Run all tests (unit + integration) @bun test src diff --git a/scripts/lint.sh b/scripts/lint.sh index 3fa7d5953..3971f3c49 100755 --- a/scripts/lint.sh +++ b/scripts/lint.sh @@ -26,9 +26,9 @@ ESLINT_PATTERN='src/**/*.{ts,tsx}' if [ "$1" = "--fix" ]; then echo "Running bun x eslint with --fix..." - bun x eslint --cache "$ESLINT_PATTERN" --fix + bun x eslint --cache --max-warnings 0 "$ESLINT_PATTERN" --fix else echo "Running eslint..." - bun x eslint --cache "$ESLINT_PATTERN" + bun x eslint --cache --max-warnings 0 "$ESLINT_PATTERN" echo "ESLint checks passed!" fi diff --git a/src/components/ChatInput.tsx b/src/components/ChatInput.tsx index c5724e608..658f7f37d 100644 --- a/src/components/ChatInput.tsx +++ b/src/components/ChatInput.tsx @@ -227,21 +227,24 @@ export const ChatInput: React.FC = ({ // Method to restore text to input (used by compaction cancel) const restoreText = useCallback( (text: string) => { - setInput(text); + setInput(() => text); focusMessageInput(); }, - [focusMessageInput] + [focusMessageInput, setInput] ); // Method to append text to input (used by Code Review notes) - const appendText = useCallback((text: string) => { - setInput((prev) => { - // Add blank line before if there's existing content - const separator = prev.trim() ? "\n\n" : ""; - return prev + separator + text; - }); - // Don't focus - user wants to keep reviewing - }, []); + const appendText = useCallback( + (text: string) => { + setInput((prev) => { + // Add blank line before if there's existing content + const separator = prev.trim() ? "\n\n" : ""; + return prev + separator + text; + }); + // Don't focus - user wants to keep reviewing + }, + [setInput] + ); // Provide API to parent via callback useEffect(() => { diff --git a/src/components/Messages/MarkdownRenderer.tsx b/src/components/Messages/MarkdownRenderer.tsx index 5e85d48b3..4b83b7b0f 100644 --- a/src/components/Messages/MarkdownRenderer.tsx +++ b/src/components/Messages/MarkdownRenderer.tsx @@ -32,19 +32,3 @@ export const PlanMarkdownContainer = styled.div` color: var(--color-plan-mode-hover); } `; - -interface PlanMarkdownRendererProps { - content: string; - className?: string; -} - -export const PlanMarkdownRenderer: React.FC = ({ - content, - className, -}) => { - return ( - - - - ); -}; diff --git a/src/components/Messages/TypewriterText.tsx b/src/components/Messages/TypewriterText.tsx deleted file mode 100644 index bc6b9749e..000000000 --- a/src/components/Messages/TypewriterText.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import React, { useEffect, useRef, useState } from "react"; -import styled from "@emotion/styled"; - -const TypewriterContainer = styled.div` - font-family: var(--font-monospace); - white-space: pre-wrap; - word-wrap: break-word; - line-height: 1.5; - color: #cccccc; -`; - -const CursorSpan = styled.span<{ show: boolean }>` - opacity: ${(props) => (props.show ? 1 : 0)}; - transition: opacity 0.1s; - &::after { - content: "|"; - color: #007acc; - font-weight: bold; - } -`; - -interface TypewriterTextProps { - deltas: string[]; - isComplete: boolean; - speed?: number; // characters per second -} - -export const TypewriterText: React.FC = ({ - deltas, - isComplete, - speed = 50, -}) => { - const containerRef = useRef(null); - const [displayedContent, setDisplayedContent] = useState(""); - const [showCursor, setShowCursor] = useState(true); - const pendingCharsRef = useRef(""); - const currentIndexRef = useRef(0); - const animationRef = useRef(); - - // Join all deltas to get full target content, ensuring strings - const targetContent = deltas - .map((delta) => (typeof delta === "string" ? delta : JSON.stringify(delta))) - .join(""); - - // Update pending characters when new deltas arrive - useEffect(() => { - pendingCharsRef.current = targetContent.slice(currentIndexRef.current); - }, [targetContent]); - - // Character-by-character rendering animation - useEffect(() => { - if (isComplete && displayedContent === targetContent) { - // Streaming complete and all text displayed - setShowCursor(false); - return; - } - - const renderNextChar = () => { - if (currentIndexRef.current < targetContent.length) { - const nextChar = targetContent[currentIndexRef.current]; - currentIndexRef.current++; - - setDisplayedContent((prev) => prev + nextChar); - - // Schedule next character - const delay = 1000 / speed; // Convert speed to milliseconds per character - animationRef.current = window.setTimeout(renderNextChar, delay); - } else if (isComplete) { - // Streaming complete and all text displayed - setShowCursor(false); - } - }; - - // Start animation if we have pending characters - if (pendingCharsRef.current.length > 0) { - if (animationRef.current) { - clearTimeout(animationRef.current); - } - renderNextChar(); - } - - // Cleanup on unmount - return () => { - if (animationRef.current) { - clearTimeout(animationRef.current); - } - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [targetContent, speed, isComplete]); - - // Cursor blinking effect - only depends on isComplete to avoid infinite loops - useEffect(() => { - if (isComplete) { - setShowCursor(false); - return; - } - - // Start blinking cursor - setShowCursor(true); - const interval = setInterval(() => { - setShowCursor((prev) => !prev); - }, 500); - - return () => clearInterval(interval); - }, [isComplete]); - - return ( - - {displayedContent} - {!isComplete && } - - ); -}; diff --git a/src/components/RightSidebar/ToolsTab.tsx b/src/components/RightSidebar/ToolsTab.tsx deleted file mode 100644 index 45ee70c35..000000000 --- a/src/components/RightSidebar/ToolsTab.tsx +++ /dev/null @@ -1,25 +0,0 @@ -import React from "react"; -import styled from "@emotion/styled"; - -const PlaceholderContainer = styled.div` - color: #888888; - font-family: var(--font-primary); - font-size: 13px; - line-height: 1.6; -`; - -const PlaceholderTitle = styled.h3` - color: #cccccc; - font-size: 14px; - font-weight: 600; - margin: 0 0 10px 0; -`; - -export const ToolsTab: React.FC = () => { - return ( - - Tools -

Tools information will be displayed here.

-
- ); -}; diff --git a/src/git.ts b/src/git.ts index 3caf40cfe..cd5ac1027 100644 --- a/src/git.ts +++ b/src/git.ts @@ -153,99 +153,6 @@ export async function createWorktree( } } -export async function removeWorktree( - projectPath: string, - workspacePath: string, - options: { force: boolean } = { force: false } -): Promise { - try { - // Remove the worktree (from the main repository context) - using proc = execAsync( - `git -C "${projectPath}" worktree remove "${workspacePath}" ${options.force ? "--force" : ""}` - ); - await proc.result; - return { success: true }; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - return { success: false, error: message }; - } -} - -export async function pruneWorktrees(projectPath: string): Promise { - try { - using proc = execAsync(`git -C "${projectPath}" worktree prune`); - await proc.result; - return { success: true }; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - return { success: false, error: message }; - } -} - -export async function moveWorktree( - projectPath: string, - oldPath: string, - newPath: string -): Promise { - try { - // Check if new path already exists - if (fs.existsSync(newPath)) { - return { - success: false, - error: `Target path already exists: ${newPath}`, - }; - } - - // Create parent directory for new path if needed - const parentDir = path.dirname(newPath); - if (!fs.existsSync(parentDir)) { - fs.mkdirSync(parentDir, { recursive: true }); - } - - // Move the worktree using git (from the main repository context) - using proc = execAsync(`git -C "${projectPath}" worktree move "${oldPath}" "${newPath}"`); - await proc.result; - return { success: true, path: newPath }; - } catch (error) { - const message = error instanceof Error ? error.message : String(error); - return { success: false, error: message }; - } -} - -export async function listWorktrees(projectPath: string): Promise { - try { - using proc = execAsync(`git -C "${projectPath}" worktree list --porcelain`); - const { stdout } = await proc.result; - const worktrees: string[] = []; - const lines = stdout.split("\n"); - - for (const line of lines) { - if (line.startsWith("worktree ")) { - const path = line.slice("worktree ".length); - if (path !== projectPath) { - // Exclude main worktree - worktrees.push(path); - } - } - } - - return worktrees; - } catch (error) { - console.error("Error listing worktrees:", error); - return []; - } -} - -export async function isGitRepository(projectPath: string): Promise { - try { - using proc = execAsync(`git -C "${projectPath}" rev-parse --git-dir`); - await proc.result; - return true; - } catch { - return false; - } -} - /** * Get the main repository path from a worktree path * @param worktreePath Path to a git worktree diff --git a/src/services/tools/fileCommon.ts b/src/services/tools/fileCommon.ts index 28c18713a..0d451ac1c 100644 --- a/src/services/tools/fileCommon.ts +++ b/src/services/tools/fileCommon.ts @@ -1,4 +1,3 @@ -import * as crypto from "crypto"; import type * as fs from "fs"; import * as path from "path"; import { createPatch } from "diff"; @@ -19,15 +18,7 @@ export const MAX_FILE_SIZE = 1024 * 1024; // 1MB * Compute a 6-character hexadecimal lease from file content. * The lease changes when file content is modified. * Uses a deterministic hash so leases are consistent across processes. - * - * @param content - File content as string or Buffer - * @returns 6-character hexadecimal lease string */ -export function leaseFromContent(content: string | Buffer): string { - // Use deterministic SHA-256 hash of content so leases are consistent - // across processes and restarts - return crypto.createHash("sha256").update(content).digest("hex").slice(0, 6); -} /** * Generate a unified diff between old and new content using jsdiff. diff --git a/src/telemetry/utils.ts b/src/telemetry/utils.ts index fcfb08eda..12864eced 100644 --- a/src/telemetry/utils.ts +++ b/src/telemetry/utils.ts @@ -9,8 +9,9 @@ import { VERSION } from "../version"; * Get base telemetry properties included with all events */ export function getBaseTelemetryProperties(): BaseTelemetryProperties { + const gitDescribe: string = VERSION.git_describe; return { - version: VERSION.git_describe, + version: gitDescribe, platform: window.api?.platform || "unknown", electronVersion: window.api?.versions?.electron || "unknown", }; diff --git a/src/types/errors.ts b/src/types/errors.ts index 4b575d435..1231ec4dc 100644 --- a/src/types/errors.ts +++ b/src/types/errors.ts @@ -14,22 +14,6 @@ export type SendMessageError = | { type: "invalid_model_string"; message: string } | { type: "unknown"; raw: string }; -/** - * Type guard to check if error is an API key error - */ -export function isApiKeyError( - error: SendMessageError -): error is { type: "api_key_not_found"; provider: string } { - return error.type === "api_key_not_found"; -} - -/** - * Type guard to check if error is an unknown error - */ -export function isUnknownError(error: SendMessageError): error is { type: "unknown"; raw: string } { - return error.type === "unknown"; -} - /** * Stream error types - categorizes errors during AI streaming * Used across backend (StreamManager) and frontend (StreamErrorMessage) diff --git a/src/types/toolParts.ts b/src/types/toolParts.ts index b45c43d99..ed71b8e17 100644 --- a/src/types/toolParts.ts +++ b/src/types/toolParts.ts @@ -28,11 +28,3 @@ export function isDynamicToolPart(part: unknown): part is DynamicToolPart { typeof part === "object" && part !== null && "type" in part && part.type === "dynamic-tool" ); } - -export function isDynamicToolPartAvailable(part: unknown): part is DynamicToolPartAvailable { - return isDynamicToolPart(part) && part.state === "output-available"; -} - -export function isDynamicToolPartPending(part: unknown): part is DynamicToolPartPending { - return isDynamicToolPart(part) && part.state === "input-available"; -} diff --git a/src/types/tools.ts b/src/types/tools.ts index 8c1751056..d7be2038f 100644 --- a/src/types/tools.ts +++ b/src/types/tools.ts @@ -122,16 +122,6 @@ export type FileEditToolArgs = | FileEditReplaceLinesToolArgs | FileEditInsertToolArgs; -export interface FileEditToolMessage { - toolName: FileEditToolName; - args: FileEditToolArgs; - result?: FileEditSharedToolResult; -} - -export function isFileEditToolName(value: string): value is FileEditToolName { - return (FILE_EDIT_TOOL_NAMES as readonly string[]).includes(value); -} - // Propose Plan Tool Types export interface ProposePlanToolArgs { title: string; @@ -159,7 +149,3 @@ export interface TodoWriteToolResult { success: true; count: number; } - -export interface TodoReadToolResult { - todos: TodoItem[]; -} diff --git a/src/types/workspace.ts b/src/types/workspace.ts index 5198f2364..4dca240b7 100644 --- a/src/types/workspace.ts +++ b/src/types/workspace.ts @@ -75,20 +75,3 @@ export interface FrontendWorkspaceMetadata extends WorkspaceMetadata { * @deprecated Use FrontendWorkspaceMetadata instead */ export type WorkspaceMetadataWithPaths = FrontendWorkspaceMetadata; - -/** - * Frontend-enriched workspace metadata with additional UI-specific data. - * Extends backend WorkspaceMetadata with frontend-computed information. - */ -export interface DisplayedWorkspaceMetadata extends FrontendWorkspaceMetadata { - /** Git status relative to origin's primary branch (null if not available) */ - gitStatus: GitStatus | null; -} - -/** - * Event emitted when workspace metadata changes - */ -export interface WorkspaceMetadataUpdate { - workspaceId: string; - metadata: WorkspaceMetadata; -} diff --git a/src/utils/ai/providerFactory.ts b/src/utils/ai/providerFactory.ts index a39455d22..859fe20a2 100644 --- a/src/utils/ai/providerFactory.ts +++ b/src/utils/ai/providerFactory.ts @@ -5,8 +5,6 @@ * lazy-loaded on first use to minimize startup time. */ -import type { LanguageModel } from "ai"; - /** * Configuration for provider creation */ @@ -20,56 +18,3 @@ export interface ProviderFactoryConfig { /** Custom fetch implementation */ fetch?: typeof fetch; } - -/** - * Create a language model instance for the given provider - * - * This function lazy-loads the provider SDK on first use. Only the requested - * provider's code is loaded, reducing startup time. - * - * @param modelString Full model string in format "provider:model-id" - * @param config Provider configuration - * @returns Promise resolving to language model instance - * @throws Error if provider is unknown or model string is invalid - */ -export async function createProviderModel( - modelString: string, - config: ProviderFactoryConfig -): Promise { - const [provider, modelId] = modelString.split(":"); - - if (!provider || !modelId) { - throw new Error(`Invalid model string: ${modelString}. Expected format: "provider:model-id"`); - } - - switch (provider) { - case "anthropic": { - const { createAnthropic } = await import("@ai-sdk/anthropic"); - const anthropicProvider = createAnthropic({ - apiKey: config.apiKey, - baseURL: config.baseURL, - headers: config.headers, - fetch: config.fetch, - }); - return anthropicProvider(modelId); - } - - case "openai": { - const { createOpenAI } = await import("@ai-sdk/openai"); - - const openaiProvider = createOpenAI({ - apiKey: config.apiKey, - baseURL: config.baseURL, - headers: config.headers, - fetch: config.fetch, - }); - - // OpenAI Responses API uses previousResponseId for conversation persistence - // No middleware needed - reasoning state is managed via previousResponseId - return openaiProvider(modelId); - } - - default: - throw new Error(`Unknown provider: ${provider}. Supported providers: anthropic, openai`); - } -} diff --git a/src/utils/chatCommands.ts b/src/utils/chatCommands.ts index 9e19c6779..0acdc412a 100644 --- a/src/utils/chatCommands.ts +++ b/src/utils/chatCommands.ts @@ -104,7 +104,7 @@ export function formatNewCommand( // Workspace Forking (re-exported from workspaceFork for convenience) // ============================================================================ -export { forkWorkspace, type ForkOptions, type ForkResult } from "./workspaceFork"; +export { forkWorkspace } from "./workspaceFork"; // ============================================================================ // Compaction diff --git a/src/utils/main/instructionFiles.ts b/src/utils/main/instructionFiles.ts index b0f991060..e16136ba8 100644 --- a/src/utils/main/instructionFiles.ts +++ b/src/utils/main/instructionFiles.ts @@ -44,20 +44,7 @@ export async function readFirstAvailableFile( * * Local files allow users to keep personal preferences separate from * shared team instructions (e.g., add AGENTS.local.md to .gitignore). - * - * @param directory - Directory to search in - * @returns Content of the local instruction file, or null if it doesn't exist */ -export async function readLocalInstructionFile(directory: string): Promise { - try { - const localFilePath = path.join(directory, LOCAL_INSTRUCTION_FILENAME); - const content = await fs.readFile(localFilePath, "utf-8"); - return content; - } catch { - // Local file doesn't exist, which is fine - return null; - } -} /** * Reads a base file with an optional local variant and returns their combined content. diff --git a/src/utils/messages/messageUtils.ts b/src/utils/messages/messageUtils.ts index 2d462cb10..22bcfd229 100644 --- a/src/utils/messages/messageUtils.ts +++ b/src/utils/messages/messageUtils.ts @@ -1,14 +1,4 @@ -import type { CmuxMessage, CmuxTextPart, DisplayedMessage } from "@/types/message"; - -/** - * Extracts text content from message parts - */ -export function extractTextContent(message: CmuxMessage): string { - return message.parts - .filter((p): p is CmuxTextPart => p.type === "text") - .map((p) => p.text || "") - .join(""); -} +import type { DisplayedMessage } from "@/types/message"; /** * Determines if the interrupted barrier should be shown for a DisplayedMessage. @@ -43,13 +33,6 @@ export function isStreamingPart(part: unknown): part is { type: "text"; state: " ); } -/** - * Checks if a message is currently streaming - */ -export function isStreamingMessage(message: CmuxMessage): boolean { - return message.parts.some(isStreamingPart); -} - /** * Merges consecutive stream-error messages with identical content. * Returns a new array where consecutive identical errors are represented as a single message diff --git a/src/utils/messages/modelMessageTransform.ts b/src/utils/messages/modelMessageTransform.ts index 2451cfe45..0fbc76349 100644 --- a/src/utils/messages/modelMessageTransform.ts +++ b/src/utils/messages/modelMessageTransform.ts @@ -31,36 +31,6 @@ export function filterEmptyAssistantMessages(messages: CmuxMessage[]): CmuxMessa }); } -/** - * Strip reasoning parts from messages for OpenAI. - * - * OpenAI's Responses API uses encrypted reasoning items (with IDs like rs_*) that are - * managed automatically via previous_response_id. When reasoning parts from history - * (which are Anthropic-style text-based reasoning) are sent to OpenAI, they create - * orphaned reasoning items that cause "reasoning without following item" errors. - * - * Anthropic's reasoning (text-based) is different and SHOULD be sent back via sendReasoning. - * - * @param messages - Messages that may contain reasoning parts - * @returns Messages with reasoning parts stripped (for OpenAI only) - */ -export function stripReasoningForOpenAI(messages: CmuxMessage[]): CmuxMessage[] { - return messages.map((msg) => { - // Only process assistant messages - if (msg.role !== "assistant") { - return msg; - } - - // Strip reasoning parts - OpenAI manages reasoning via previousResponseId - const filteredParts = msg.parts.filter((part) => part.type !== "reasoning"); - - return { - ...msg, - parts: filteredParts, - }; - }); -} - /** * Add [CONTINUE] sentinel to partial messages by inserting a user message. * This helps the model understand that a message was interrupted and to continue. diff --git a/src/utils/providers/ensureProvidersConfig.ts b/src/utils/providers/ensureProvidersConfig.ts index 0b205dcfa..915de62c2 100644 --- a/src/utils/providers/ensureProvidersConfig.ts +++ b/src/utils/providers/ensureProvidersConfig.ts @@ -104,9 +104,3 @@ export const ensureProvidersConfig = ( config.saveProvidersConfig(providersFromEnv); return providersFromEnv; }; - -export const getProvidersFromEnv = (env: NodeJS.ProcessEnv = process.env): ProvidersConfig => - buildProvidersFromEnv(env); - -export const hasAnyProvidersConfigured = (providers: ProvidersConfig | null | undefined): boolean => - hasAnyConfiguredProvider(providers); diff --git a/src/utils/slashCommands/parser.ts b/src/utils/slashCommands/parser.ts index 8cb5a3cab..e3a174750 100644 --- a/src/utils/slashCommands/parser.ts +++ b/src/utils/slashCommands/parser.ts @@ -85,34 +85,6 @@ export function parseCommand(input: string): ParsedCommand { }); } -/** - * Set a nested property value using a key path - * @param obj The object to modify - * @param keyPath Array of keys representing the path (e.g., ["baseUrl", "scheme"]) - * @param value The value to set - */ -export function setNestedProperty( - obj: Record, - keyPath: string[], - value: string -): void { - if (keyPath.length === 0) { - return; - } - - let current = obj; - for (let i = 0; i < keyPath.length - 1; i++) { - const key = keyPath[i]; - if (!(key in current) || typeof current[key] !== "object" || current[key] === null) { - current[key] = {}; - } - current = current[key] as Record; - } - - const lastKey = keyPath[keyPath.length - 1]; - current[lastKey] = value; -} - /** * Get slash command definitions for use in suggestions */ diff --git a/src/utils/ui/dateTime.ts b/src/utils/ui/dateTime.ts index 4502a4e7d..a668dc4b1 100644 --- a/src/utils/ui/dateTime.ts +++ b/src/utils/ui/dateTime.ts @@ -35,26 +35,6 @@ export function formatTimestamp(timestamp: number): string { } } -/** - * Formats a Unix timestamp (milliseconds) into a full date/time string with high precision. - * Used for tooltips and detailed views. - * - * @param timestamp Unix timestamp in milliseconds - * @returns Formatted full timestamp string (e.g., "October 23, 2025, 8:13:42 PM") - */ -export function formatFullTimestamp(timestamp: number): string { - const date = new Date(timestamp); - return date.toLocaleString("en-US", { - year: "numeric", - month: "long", - day: "numeric", - hour: "numeric", - minute: "2-digit", - second: "2-digit", - hour12: true, - }); -} - /** * Formats a Unix timestamp (milliseconds) into a human-readable relative time string. * Examples: "2 minutes ago", "3 hours ago", "2 days ago", "3 weeks ago" diff --git a/src/utils/vim.ts b/src/utils/vim.ts index 763a454dd..000a2ed0a 100644 --- a/src/utils/vim.ts +++ b/src/utils/vim.ts @@ -65,17 +65,6 @@ export function indexAt(text: string, row: number, col: number): number { return starts[row] + col; } -/** - * Get the end index of the line containing idx. - */ -export function lineEndAtIndex(text: string, idx: number): number { - const { lines, starts } = getLinesInfo(text); - let row = 0; - while (row + 1 < starts.length && starts[row + 1] <= idx) row++; - const lineEnd = starts[row] + lines[row].length; - return lineEnd; -} - /** * Get line bounds (start, end) for the line containing cursor. */