From 1eaa9a90331a8c9d9b016fc7244494c956735190 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Thu, 9 Oct 2025 11:11:05 +0200 Subject: [PATCH] style: run `make fmt` on main Applied consistent whitespace and JSX wrapping to improve readability. Aligned tooltip link formatting and string literals with project standards. Cleaned doc markdown spacing to avoid stray blank lines and indentation. --- docs/AGENTS.md | 10 ++- docs/vim-mode.md | 19 ++++- src/components/AIView.tsx | 7 +- src/components/ChatInput.tsx | 3 +- src/components/ChatMetaSidebar/CostsTab.tsx | 14 +--- src/components/ChatToggles.tsx | 6 +- src/components/Context1MCheckbox.tsx | 27 ++++--- src/components/Messages/MarkdownCore.tsx | 2 +- src/components/Messages/MarkdownRenderer.tsx | 5 +- src/components/Messages/ToolMessage.tsx | 5 +- src/components/VimTextArea.tsx | 30 ++++--- src/components/tools/FileReadToolCall.tsx | 3 +- src/main.ts | 10 ++- src/services/aiService.ts | 14 ++-- src/services/tools/bash.test.ts | 17 ++-- src/utils/ai/models.ts | 3 +- src/utils/tokens/modelStats.test.ts | 8 +- src/utils/vim.test.ts | 82 ++++++++++---------- src/utils/vim.ts | 58 ++++++-------- tests/ipcMain/anthropic1MContext.test.ts | 26 +++---- tests/ipcMain/helpers.ts | 15 ++-- tests/ipcMain/sendMessage.test.ts | 1 - 22 files changed, 190 insertions(+), 175 deletions(-) diff --git a/docs/AGENTS.md b/docs/AGENTS.md index e35e3c261..dd6eea799 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -71,6 +71,7 @@ Write PR bodies for **busy reviewers**. Be concise and avoid redundancy: - **If it's obvious, omit it** - Problem obvious from solution? Don't state it. Solution obvious from problem? Skip to implementation details. ❌ **Bad** (redundant): + ``` Problem: Markdown rendering is slow, causing 50ms tasks Solution: Make markdown rendering faster @@ -78,6 +79,7 @@ Impact: Reduces task time to <16ms ``` ✅ **Good** (each section adds value): + ``` ReactMarkdown was re-parsing content on every parent render because plugin arrays were created fresh each time. Moved to module scope for stable references. @@ -104,8 +106,8 @@ Verify with React DevTools Profiler - MarkdownCore should only re-render when co - User docs are built with mdbook and deployed to https://cmux.io - Must be added to `docs/SUMMARY.md` to appear in the docs - Use standard markdown + mermaid diagrams -- **Developer docs** → inline with the code its documenting as comments. Consider them notes as notes to future Assistants to understand the logic more quickly. -**DO NOT** create standalone documentation files in the project root or random locations. +- **Developer docs** → inline with the code its documenting as comments. Consider them notes as notes to future Assistants to understand the logic more quickly. + **DO NOT** create standalone documentation files in the project root or random locations. ### External API Docs @@ -162,10 +164,10 @@ This project uses **Make** as the primary build orchestrator. See `Makefile` for ### Test-Driven Development (TDD) -**TDD is the preferred development style for agents.** +**TDD is the preferred development style for agents.** - Prefer relocated complex logic into places where they're easily tested - - E.g. pure functions in `utils` are easier to test than complex logic in a React component + - E.g. pure functions in `utils` are easier to test than complex logic in a React component - Strive for broad coverage with minimal tests - Prefer testing large blocks of composite logic - Tests should be written with the end-user experience in mind diff --git a/docs/vim-mode.md b/docs/vim-mode.md index 0c94e5df0..9e3a854a0 100644 --- a/docs/vim-mode.md +++ b/docs/vim-mode.md @@ -15,11 +15,13 @@ Vim mode is always enabled. Press **ESC** to enter normal mode from insert mode. ## Modes ### Insert Mode (Default) + - This is the default mode when typing in the chat input - Type normally, all characters are inserted - Press **ESC** or **Ctrl-[** to enter normal mode ### Normal Mode + - Command mode for navigation and editing - Indicated by "NORMAL" text above the input - Pending commands are shown (e.g., "NORMAL d" when delete is pending) @@ -28,12 +30,14 @@ Vim mode is always enabled. Press **ESC** to enter normal mode from insert mode. ## Navigation ### Basic Movement + - **h** - Move left one character - **j** - Move down one line - **k** - Move up one line - **l** - Move right one character ### Word Movement + - **w** - Move forward to start of next word - **W** - Move forward to start of next WORD (whitespace-separated) - **b** - Move backward to start of previous word @@ -42,12 +46,14 @@ Vim mode is always enabled. Press **ESC** to enter normal mode from insert mode. - **E** - Move to end of current/next WORD ### Line Movement + - **0** - Move to beginning of line - **$** - Move to end of line - **Home** - Same as **0** - **End** - Same as **$** ### Column Preservation + When moving up/down with **j**/**k**, the cursor attempts to stay in the same column position. If a line is shorter, the cursor moves to the end of that line, but will return to the original column on longer lines. ## Entering Insert Mode @@ -62,15 +68,18 @@ When moving up/down with **j**/**k**, the cursor attempts to stay in the same co ## Editing Commands ### Simple Edits + - **x** - Delete character under cursor - **p** - Paste after cursor - **P** - Paste before cursor ### Undo/Redo + - **u** - Undo last change - **Ctrl-r** - Redo ### Line Operations + - **dd** - Delete line (yank to clipboard) - **yy** - Yank (copy) line - **cc** - Change line (delete and enter insert mode) @@ -80,11 +89,13 @@ When moving up/down with **j**/**k**, the cursor attempts to stay in the same co Vim's power comes from combining operators with motions. All operators work with all motions: ### Operators + - **d** - Delete - **c** - Change (delete and enter insert mode) - **y** - Yank (copy) ### Motions + - **w** - To next word - **b** - To previous word - **e** - To end of word @@ -92,6 +103,7 @@ Vim's power comes from combining operators with motions. All operators work with - **0** - To beginning of line ### Examples + - **dw** - Delete to next word - **de** - Delete to end of word - **d$** - Delete to end of line @@ -103,6 +115,7 @@ Vim's power comes from combining operators with motions. All operators work with - **yy** - Yank line (doubled operator) ### Shortcuts + - **D** - Same as **d$** (delete to end of line) - **C** - Same as **c$** (change to end of line) @@ -111,6 +124,7 @@ Vim's power comes from combining operators with motions. All operators work with Text objects let you operate on semantic units: ### Inner Word (iw) + - **diw** - Delete inner word (word under cursor) - **ciw** - Change inner word - **yiw** - Yank inner word @@ -125,13 +139,13 @@ Text objects work from anywhere within the word - you don't need to be at the st ## Keybind Conflicts ### ESC Key + ESC is used for: + 1. Exiting Vim normal mode (highest priority) 2. NOT used for canceling edits (use **Ctrl-Q** instead) 3. NOT used for interrupting streams (use **Ctrl-C** instead) - - ## Tips 1. **Learn operators + motions**: Instead of memorizing every command, learn the operators (d, c, y) and motions (w, b, $, 0). They combine naturally. @@ -143,6 +157,7 @@ ESC is used for: ## Not Yet Implemented Features that may be added in the future: + - **ge** - Backward end of word motion - **f{char}**, **t{char}** - Find character motions - **i"**, **i'**, **i(**, **i[**, **i{** - More text objects diff --git a/src/components/AIView.tsx b/src/components/AIView.tsx index 050cc08b9..01e60f3e8 100644 --- a/src/components/AIView.tsx +++ b/src/components/AIView.tsx @@ -320,7 +320,12 @@ const AIViewInner: React.FC = ({ } return ( - + diff --git a/src/components/ChatInput.tsx b/src/components/ChatInput.tsx index ad47eb5c8..f0ab2231f 100644 --- a/src/components/ChatInput.tsx +++ b/src/components/ChatInput.tsx @@ -742,7 +742,8 @@ export const ChatInput: React.FC = ({ ? - Click to edit or use {formatKeybind(KEYBINDS.OPEN_MODEL_SELECTOR)} + Click to edit or use{" "} + {formatKeybind(KEYBINDS.OPEN_MODEL_SELECTOR)}

Abbreviations: diff --git a/src/components/ChatMetaSidebar/CostsTab.tsx b/src/components/ChatMetaSidebar/CostsTab.tsx index 3e02cca4b..35979f381 100644 --- a/src/components/ChatMetaSidebar/CostsTab.tsx +++ b/src/components/ChatMetaSidebar/CostsTab.tsx @@ -237,8 +237,6 @@ const SectionHeader = styled.div` margin-bottom: 12px; `; - - // Format token display - show k for thousands with 1 decimal const formatTokens = (tokens: number) => tokens >= 1000 ? `${(tokens / 1000).toFixed(1)}k` : tokens.toLocaleString(); @@ -263,11 +261,7 @@ const formatCostWithDollar = (cost: number | undefined): string => { * Calculate cost with elevated pricing for 1M context (200k-1M tokens) * For tokens above 200k, use elevated pricing rates */ -const calculateElevatedCost = ( - tokens: number, - standardRate: number, - isInput: boolean -): number => { +const calculateElevatedCost = (tokens: number, standardRate: number, isInput: boolean): number => { if (tokens <= 200_000) { return tokens * standardRate; } @@ -408,19 +402,19 @@ export const CostsTab: React.FC = () => { adjustedInputCost = calculateElevatedCost( displayUsage.input.tokens, modelStats.input_cost_per_token, - true // isInput + true // isInput ); // Recalculate output cost with elevated pricing adjustedOutputCost = calculateElevatedCost( displayUsage.output.tokens, modelStats.output_cost_per_token, - false // isOutput + false // isOutput ); // Recalculate reasoning cost with elevated pricing adjustedReasoningCost = calculateElevatedCost( displayUsage.reasoning.tokens, modelStats.output_cost_per_token, - false // isOutput + false // isOutput ); } diff --git a/src/components/ChatToggles.tsx b/src/components/ChatToggles.tsx index f410aa210..1ac5e7c20 100644 --- a/src/components/ChatToggles.tsx +++ b/src/components/ChatToggles.tsx @@ -15,11 +15,7 @@ interface ChatTogglesProps { children: React.ReactNode; } -export const ChatToggles: React.FC = ({ - workspaceId, - modelString, - children, -}) => { +export const ChatToggles: React.FC = ({ workspaceId, modelString, children }) => { return ( {children} diff --git a/src/components/Context1MCheckbox.tsx b/src/components/Context1MCheckbox.tsx index 6df23a6f4..10642070b 100644 --- a/src/components/Context1MCheckbox.tsx +++ b/src/components/Context1MCheckbox.tsx @@ -35,18 +35,18 @@ const Checkbox = styled.input` border-radius: 2px; background: #1e1e1e; position: relative; - + &:hover { border-color: #007acc; } - + &:checked { background: #007acc; border-color: #007acc; } - + &:checked::after { - content: ''; + content: ""; position: absolute; left: 3px; top: 0px; @@ -77,15 +77,22 @@ export const Context1MCheckbox: React.FC = ({ return ( - setUse1M(e.target.checked)} - /> + setUse1M(e.target.checked)} /> 1M Context - ? + + ? + Enable 1M token context window (beta feature for Claude Sonnet 4/4.5) diff --git a/src/components/Messages/MarkdownCore.tsx b/src/components/Messages/MarkdownCore.tsx index ef828ff39..f5e90d76d 100644 --- a/src/components/Messages/MarkdownCore.tsx +++ b/src/components/Messages/MarkdownCore.tsx @@ -20,7 +20,7 @@ const REHYPE_PLUGINS = [rehypeKatex]; /** * Core markdown rendering component that handles all markdown processing. * This is the single source of truth for markdown configuration. - * + * * Memoized to prevent expensive re-parsing when content hasn't changed. */ export const MarkdownCore = React.memo(({ content, children }) => { diff --git a/src/components/Messages/MarkdownRenderer.tsx b/src/components/Messages/MarkdownRenderer.tsx index 0d74218ea..5e85d48b3 100644 --- a/src/components/Messages/MarkdownRenderer.tsx +++ b/src/components/Messages/MarkdownRenderer.tsx @@ -38,7 +38,10 @@ interface PlanMarkdownRendererProps { className?: string; } -export const PlanMarkdownRenderer: React.FC = ({ content, className }) => { +export const PlanMarkdownRenderer: React.FC = ({ + content, + className, +}) => { return ( diff --git a/src/components/Messages/ToolMessage.tsx b/src/components/Messages/ToolMessage.tsx index e53667db4..607a1e397 100644 --- a/src/components/Messages/ToolMessage.tsx +++ b/src/components/Messages/ToolMessage.tsx @@ -37,10 +37,7 @@ function isBashTool(toolName: string, args: unknown): args is BashToolArgs { // Type guard for file_read tool function isFileReadTool(toolName: string, args: unknown): args is FileReadToolArgs { return ( - toolName === "file_read" && - typeof args === "object" && - args !== null && - "filePath" in args + toolName === "file_read" && typeof args === "object" && args !== null && "filePath" in args ); } diff --git a/src/components/VimTextArea.tsx b/src/components/VimTextArea.tsx index 3dc79997f..d189ad9ae 100644 --- a/src/components/VimTextArea.tsx +++ b/src/components/VimTextArea.tsx @@ -106,13 +106,16 @@ export const VimTextArea = React.forwardRef { if (!ref) return; if (typeof ref === "function") ref(textareaRef.current); - else - (ref).current = textareaRef.current; + else ref.current = textareaRef.current; }, [ref]); const [vimMode, setVimMode] = useState("insert"); const [desiredColumn, setDesiredColumn] = useState(null); - const [pendingOp, setPendingOp] = useState(null); + const [pendingOp, setPendingOp] = useState(null); const yankBufferRef = useRef(""); // Auto-resize when value changes @@ -124,8 +127,6 @@ export const VimTextArea = React.forwardRef new Set(suppressKeys ?? []), [suppressKeys]); const withSelection = () => { @@ -189,7 +190,7 @@ export const VimTextArea = React.forwardRef setCursor(newState.cursor, newState.mode), 0); @@ -226,10 +227,21 @@ export const VimTextArea = React.forwardRefVim Mode Enabled

- Press ESC for normal mode, i to return to insert mode. + Press ESC for normal mode, i to return to insert + mode.

- See { e.preventDefault(); window.open('/docs/vim-mode.md'); }}>Vim Mode docs for full command reference. + See{" "} + { + e.preventDefault(); + window.open("/docs/vim-mode.md"); + }} + > + Vim Mode docs + {" "} + for full command reference.
normal diff --git a/src/components/tools/FileReadToolCall.tsx b/src/components/tools/FileReadToolCall.tsx index edbeddf62..3fbb2a5ee 100644 --- a/src/components/tools/FileReadToolCall.tsx +++ b/src/components/tools/FileReadToolCall.tsx @@ -159,8 +159,7 @@ export const FileReadToolCall: React.FC = ({ const fileName = args.filePath.split("/").pop() ?? args.filePath; // Parse the file content to extract line numbers and actual content - const parsedContent = - result?.success && result.content ? parseFileContent(result.content) : null; + const parsedContent = result?.success && result.content ? parseFileContent(result.content) : null; return ( diff --git a/src/main.ts b/src/main.ts index 7d00ecfe9..e13d611c0 100644 --- a/src/main.ts +++ b/src/main.ts @@ -40,7 +40,9 @@ if (!app.isPackaged) { const config = new Config(); const ipcMain = new IpcMain(config); -console.log(`Cmux starting - version: ${(VERSION as { git?: string; buildTime?: string }).git ?? "(dev)"} (built: ${(VERSION as { git?: string; buildTime?: string }).buildTime ?? "dev-mode"})`); +console.log( + `Cmux starting - version: ${(VERSION as { git?: string; buildTime?: string }).git ?? "(dev)"} (built: ${(VERSION as { git?: string; buildTime?: string }).buildTime ?? "dev-mode"})` +); console.log("Main process starting..."); // Global error handlers for better error reporting @@ -189,7 +191,7 @@ function createWindow() { // Development mode: load from vite dev server void mainWindow.loadURL("http://localhost:5173"); // Open DevTools after React content loads - mainWindow.webContents.once('did-finish-load', () => { + mainWindow.webContents.once("did-finish-load", () => { mainWindow?.webContents.openDevTools(); }); } @@ -203,7 +205,7 @@ function createWindow() { if (gotTheLock) { void app.whenReady().then(async () => { console.log("App ready, creating window..."); - + // Install React DevTools in development if (!app.isPackaged && installExtension && REACT_DEVELOPER_TOOLS) { try { @@ -215,7 +217,7 @@ if (gotTheLock) { console.log("❌ Error installing React DevTools:", err); } } - + createMenu(); createWindow(); // No need to auto-start workspaces anymore - they start on demand diff --git a/src/services/aiService.ts b/src/services/aiService.ts index 2b69ea932..0f6cf6ab7 100644 --- a/src/services/aiService.ts +++ b/src/services/aiService.ts @@ -181,17 +181,15 @@ export class AIService extends EventEmitter { // Add 1M context beta header if requested const use1MContext = cmuxProviderOptions?.anthropic?.use1MContext; const existingHeaders = providerConfig.headers as Record | undefined; - const headers = use1MContext && existingHeaders - ? { ...existingHeaders, "anthropic-beta": "context-1m-2025-08-07" } - : use1MContext - ? { "anthropic-beta": "context-1m-2025-08-07" } - : existingHeaders; - - + const headers = + use1MContext && existingHeaders + ? { ...existingHeaders, "anthropic-beta": "context-1m-2025-08-07" } + : use1MContext + ? { "anthropic-beta": "context-1m-2025-08-07" } + : existingHeaders; // Pass configuration verbatim to the provider, ensuring parity with Vercel AI SDK - const provider = createAnthropic({ ...providerConfig, headers }); return Ok(provider(modelId)); } diff --git a/src/services/tools/bash.test.ts b/src/services/tools/bash.test.ts index 5feb0e57f..14e34dcb9 100644 --- a/src/services/tools/bash.test.ts +++ b/src/services/tools/bash.test.ts @@ -78,12 +78,14 @@ describe("bash tool", () => { if (!result.success) { expect(result.error).toContain("[OUTPUT OVERFLOW"); // Should contain specific overflow reason (one of the three types) - expect(result.error).toMatch(/Line count exceeded limit|Total output exceeded limit|exceeded per-line limit/); + expect(result.error).toMatch( + /Line count exceeded limit|Total output exceeded limit|exceeded per-line limit/ + ); expect(result.error).toContain("Full output"); expect(result.error).toContain("lines) saved to"); expect(result.error).toContain("bash-"); expect(result.error).toContain(".txt"); - + // Verify helpful filtering instructions are included expect(result.error).toContain("grep ''"); expect(result.error).toContain("head -n 300"); @@ -95,29 +97,27 @@ describe("bash tool", () => { expect(match).toBeDefined(); if (match) { const overflowPath = match[1]; - + // Verify file has short ID format (bash-<8 hex chars>.txt) const filename = overflowPath.split("/").pop(); expect(filename).toMatch(/^bash-[0-9a-f]{8}\.txt$/); - + // Verify file exists and read contents expect(fs.existsSync(overflowPath)).toBe(true); - + // Verify file contains collected lines (at least 300, may be slightly more) const fileContent = fs.readFileSync(overflowPath, "utf-8"); const fileLines = fileContent.split("\n").filter((l: string) => l.length > 0); expect(fileLines.length).toBeGreaterThanOrEqual(300); expect(fileContent).toContain("line1"); expect(fileContent).toContain("line300"); - + // Clean up temp file fs.unlinkSync(overflowPath); } } }); - - it("should fail early when max_lines is reached", async () => { const tool = createBashTool({ cwd: process.cwd() }); const startTime = performance.now(); @@ -597,5 +597,4 @@ describe("bash tool", () => { expect(result.wall_duration_ms).toBe(0); } }); - }); diff --git a/src/utils/ai/models.ts b/src/utils/ai/models.ts index d6664ae5d..907414ba3 100644 --- a/src/utils/ai/models.ts +++ b/src/utils/ai/models.ts @@ -30,7 +30,6 @@ export function supports1MContext(modelString: string): boolean { } // Check for Sonnet 4 and Sonnet 4.5 models return ( - modelName?.includes("claude-sonnet-4") && - !modelName.includes("claude-sonnet-3") // Exclude Sonnet 3.x models + modelName?.includes("claude-sonnet-4") && !modelName.includes("claude-sonnet-3") // Exclude Sonnet 3.x models ); } diff --git a/src/utils/tokens/modelStats.test.ts b/src/utils/tokens/modelStats.test.ts index fe14747c4..fc9a85aee 100644 --- a/src/utils/tokens/modelStats.test.ts +++ b/src/utils/tokens/modelStats.test.ts @@ -3,7 +3,7 @@ import { getModelStats } from "./modelStats"; describe("getModelStats", () => { it("should return model stats for claude-sonnet-4-5", () => { const stats = getModelStats("anthropic:claude-sonnet-4-5"); - + expect(stats).not.toBeNull(); expect(stats?.input_cost_per_token).toBe(0.000003); expect(stats?.output_cost_per_token).toBe(0.000015); @@ -12,21 +12,21 @@ describe("getModelStats", () => { it("should handle model without provider prefix", () => { const stats = getModelStats("claude-sonnet-4-5"); - + expect(stats).not.toBeNull(); expect(stats?.input_cost_per_token).toBe(0.000003); }); it("should return cache pricing when available", () => { const stats = getModelStats("anthropic:claude-sonnet-4-5"); - + expect(stats?.cache_creation_input_token_cost).toBe(0.00000375); expect(stats?.cache_read_input_token_cost).toBe(3e-7); }); it("should return null for unknown models", () => { const stats = getModelStats("unknown:model"); - + expect(stats).toBeNull(); }); }); diff --git a/src/utils/vim.test.ts b/src/utils/vim.test.ts index 199dace30..bb4d44e0b 100644 --- a/src/utils/vim.test.ts +++ b/src/utils/vim.test.ts @@ -36,7 +36,7 @@ function executeVimCommands(initial: vim.VimState, keys: string[]): vim.VimState const actualKey = ctrl ? key.slice(5) : key; const result = vim.handleKeyPress(state, actualKey, { ctrl }); - + if (result.handled) { // Ignore undo/redo actions in tests (they require browser execCommand) if (result.action === "undo" || result.action === "redo") { @@ -64,7 +64,7 @@ describe("Vim Command Integration Tests", () => { test("ESC enters normal mode from insert", () => { const state = executeVimCommands( { ...initialState, text: "hello", cursor: 5, mode: "insert" }, - ["Escape"], + ["Escape"] ); expect(state.mode).toBe("normal"); expect(state.cursor).toBe(4); // Clamps to last char @@ -73,7 +73,7 @@ describe("Vim Command Integration Tests", () => { test("i enters insert mode at cursor", () => { const state = executeVimCommands( { ...initialState, text: "hello", cursor: 2, mode: "normal" }, - ["i"], + ["i"] ); expect(state.mode).toBe("insert"); expect(state.cursor).toBe(2); @@ -82,7 +82,7 @@ describe("Vim Command Integration Tests", () => { test("a enters insert mode after cursor", () => { const state = executeVimCommands( { ...initialState, text: "hello", cursor: 2, mode: "normal" }, - ["a"], + ["a"] ); expect(state.mode).toBe("insert"); expect(state.cursor).toBe(3); @@ -91,7 +91,7 @@ describe("Vim Command Integration Tests", () => { test("o opens line below", () => { const state = executeVimCommands( { ...initialState, text: "hello\nworld", cursor: 2, mode: "normal" }, - ["o"], + ["o"] ); expect(state.mode).toBe("insert"); expect(state.text).toBe("hello\n\nworld"); @@ -103,7 +103,7 @@ describe("Vim Command Integration Tests", () => { test("w moves to next word", () => { const state = executeVimCommands( { ...initialState, text: "hello world foo", cursor: 0, mode: "normal" }, - ["w"], + ["w"] ); expect(state.cursor).toBe(6); }); @@ -111,7 +111,7 @@ describe("Vim Command Integration Tests", () => { test("b moves to previous word", () => { const state = executeVimCommands( { ...initialState, text: "hello world foo", cursor: 12, mode: "normal" }, - ["b"], + ["b"] ); expect(state.cursor).toBe(6); }); @@ -119,7 +119,7 @@ describe("Vim Command Integration Tests", () => { test("$ moves to end of line", () => { const state = executeVimCommands( { ...initialState, text: "hello world", cursor: 0, mode: "normal" }, - ["$"], + ["$"] ); expect(state.cursor).toBe(10); // On last char, not past it }); @@ -127,7 +127,7 @@ describe("Vim Command Integration Tests", () => { test("0 moves to start of line", () => { const state = executeVimCommands( { ...initialState, text: "hello world", cursor: 10, mode: "normal" }, - ["0"], + ["0"] ); expect(state.cursor).toBe(0); }); @@ -137,7 +137,7 @@ describe("Vim Command Integration Tests", () => { test("x deletes character under cursor", () => { const state = executeVimCommands( { ...initialState, text: "hello", cursor: 1, mode: "normal" }, - ["x"], + ["x"] ); expect(state.text).toBe("hllo"); expect(state.cursor).toBe(1); @@ -147,7 +147,7 @@ describe("Vim Command Integration Tests", () => { test("p pastes after cursor", () => { const state = executeVimCommands( { ...initialState, text: "hello", cursor: 2, mode: "normal", yankBuffer: "XX" }, - ["p"], + ["p"] ); expect(state.text).toBe("helXXlo"); expect(state.cursor).toBe(4); @@ -156,7 +156,7 @@ describe("Vim Command Integration Tests", () => { test("P pastes before cursor", () => { const state = executeVimCommands( { ...initialState, text: "hello", cursor: 2, mode: "normal", yankBuffer: "XX" }, - ["P"], + ["P"] ); expect(state.text).toBe("heXXllo"); expect(state.cursor).toBe(2); @@ -167,7 +167,7 @@ describe("Vim Command Integration Tests", () => { test("dd deletes line", () => { const state = executeVimCommands( { ...initialState, text: "hello\nworld\nfoo", cursor: 8, mode: "normal" }, - ["d", "d"], + ["d", "d"] ); expect(state.text).toBe("hello\nfoo"); expect(state.yankBuffer).toBe("world\n"); @@ -176,7 +176,7 @@ describe("Vim Command Integration Tests", () => { test("yy yanks line", () => { const state = executeVimCommands( { ...initialState, text: "hello\nworld", cursor: 2, mode: "normal" }, - ["y", "y"], + ["y", "y"] ); expect(state.text).toBe("hello\nworld"); // Text unchanged expect(state.yankBuffer).toBe("hello\n"); @@ -185,7 +185,7 @@ describe("Vim Command Integration Tests", () => { test("cc changes line", () => { const state = executeVimCommands( { ...initialState, text: "hello\nworld\nfoo", cursor: 8, mode: "normal" }, - ["c", "c"], + ["c", "c"] ); expect(state.text).toBe("hello\n\nfoo"); expect(state.mode).toBe("insert"); @@ -197,7 +197,7 @@ describe("Vim Command Integration Tests", () => { test("d$ deletes to end of line", () => { const state = executeVimCommands( { ...initialState, text: "hello world", cursor: 6, mode: "normal" }, - ["d", "$"], + ["d", "$"] ); expect(state.text).toBe("hello "); expect(state.cursor).toBe(6); @@ -207,7 +207,7 @@ describe("Vim Command Integration Tests", () => { test("D deletes to end of line (shortcut)", () => { const state = executeVimCommands( { ...initialState, text: "hello world", cursor: 6, mode: "normal" }, - ["D"], + ["D"] ); expect(state.text).toBe("hello "); expect(state.cursor).toBe(6); @@ -216,7 +216,7 @@ describe("Vim Command Integration Tests", () => { test("d0 deletes to beginning of line", () => { const state = executeVimCommands( { ...initialState, text: "hello world", cursor: 6, mode: "normal" }, - ["d", "0"], + ["d", "0"] ); expect(state.text).toBe("world"); expect(state.yankBuffer).toBe("hello "); @@ -225,7 +225,7 @@ describe("Vim Command Integration Tests", () => { test("dw deletes to next word", () => { const state = executeVimCommands( { ...initialState, text: "hello world foo", cursor: 0, mode: "normal" }, - ["d", "w"], + ["d", "w"] ); expect(state.text).toBe("world foo"); expect(state.yankBuffer).toBe("hello "); @@ -234,7 +234,7 @@ describe("Vim Command Integration Tests", () => { test("db deletes to previous word", () => { const state = executeVimCommands( { ...initialState, text: "hello world foo", cursor: 12, mode: "normal" }, - ["d", "b"], + ["d", "b"] ); expect(state.text).toBe("hello foo"); }); @@ -244,7 +244,7 @@ describe("Vim Command Integration Tests", () => { test("c$ changes to end of line", () => { const state = executeVimCommands( { ...initialState, text: "hello world", cursor: 6, mode: "normal" }, - ["c", "$"], + ["c", "$"] ); expect(state.text).toBe("hello "); expect(state.mode).toBe("insert"); @@ -254,7 +254,7 @@ describe("Vim Command Integration Tests", () => { test("C changes to end of line (shortcut)", () => { const state = executeVimCommands( { ...initialState, text: "hello world", cursor: 6, mode: "normal" }, - ["C"], + ["C"] ); expect(state.text).toBe("hello "); expect(state.mode).toBe("insert"); @@ -263,7 +263,7 @@ describe("Vim Command Integration Tests", () => { test("c0 changes to beginning of line", () => { const state = executeVimCommands( { ...initialState, text: "hello world", cursor: 6, mode: "normal" }, - ["c", "0"], + ["c", "0"] ); expect(state.text).toBe("world"); expect(state.mode).toBe("insert"); @@ -272,7 +272,7 @@ describe("Vim Command Integration Tests", () => { test("cw changes to next word", () => { const state = executeVimCommands( { ...initialState, text: "hello world", cursor: 0, mode: "normal" }, - ["c", "w"], + ["c", "w"] ); expect(state.text).toBe("world"); expect(state.mode).toBe("insert"); @@ -283,7 +283,7 @@ describe("Vim Command Integration Tests", () => { test("y$ yanks to end of line", () => { const state = executeVimCommands( { ...initialState, text: "hello world", cursor: 6, mode: "normal" }, - ["y", "$"], + ["y", "$"] ); expect(state.text).toBe("hello world"); // Text unchanged expect(state.yankBuffer).toBe("world"); @@ -293,7 +293,7 @@ describe("Vim Command Integration Tests", () => { test("y0 yanks to beginning of line", () => { const state = executeVimCommands( { ...initialState, text: "hello world", cursor: 6, mode: "normal" }, - ["y", "0"], + ["y", "0"] ); expect(state.text).toBe("hello world"); expect(state.yankBuffer).toBe("hello "); @@ -302,7 +302,7 @@ describe("Vim Command Integration Tests", () => { test("yw yanks to next word", () => { const state = executeVimCommands( { ...initialState, text: "hello world", cursor: 0, mode: "normal" }, - ["y", "w"], + ["y", "w"] ); expect(state.text).toBe("hello world"); expect(state.yankBuffer).toBe("hello "); @@ -313,7 +313,7 @@ describe("Vim Command Integration Tests", () => { test("ESC then d$ deletes from insert cursor to end", () => { const state = executeVimCommands( { ...initialState, text: "hello world", cursor: 6, mode: "insert" }, - ["Escape", "d", "$"], + ["Escape", "d", "$"] ); // Cursor at 6 in insert mode stays at 6 after ESC (on 'w') // d$ deletes from 'w' to end of line @@ -324,7 +324,7 @@ describe("Vim Command Integration Tests", () => { test("navigate with w, then delete with dw", () => { const state = executeVimCommands( { ...initialState, text: "one two three", cursor: 0, mode: "normal" }, - ["w", "d", "w"], + ["w", "d", "w"] ); expect(state.text).toBe("one three"); }); @@ -332,7 +332,7 @@ describe("Vim Command Integration Tests", () => { test("yank line, navigate, paste", () => { const state = executeVimCommands( { ...initialState, text: "first\nsecond\nthird", cursor: 0, mode: "normal" }, - ["y", "y", "j", "j", "p"], + ["y", "y", "j", "j", "p"] ); expect(state.yankBuffer).toBe("first\n"); // After yy: cursor at 0, yank "first\n" @@ -345,7 +345,7 @@ describe("Vim Command Integration Tests", () => { test("delete word, move, paste", () => { const state = executeVimCommands( { ...initialState, text: "hello world foo", cursor: 0, mode: "normal" }, - ["d", "w", "w", "p"], + ["d", "w", "w", "p"] ); expect(state.yankBuffer).toBe("hello "); // After dw: text = "world foo", cursor at 0, yank "hello " @@ -359,7 +359,7 @@ describe("Vim Command Integration Tests", () => { test("$ on empty line", () => { const state = executeVimCommands( { ...initialState, text: "hello\n\nworld", cursor: 6, mode: "normal" }, - ["$"], + ["$"] ); expect(state.cursor).toBe(6); // Empty line, stays at newline char }); @@ -367,7 +367,7 @@ describe("Vim Command Integration Tests", () => { test("w at end of text", () => { const state = executeVimCommands( { ...initialState, text: "hello", cursor: 4, mode: "normal" }, - ["w"], + ["w"] ); expect(state.cursor).toBe(4); // Clamps to last char }); @@ -375,7 +375,7 @@ describe("Vim Command Integration Tests", () => { test("d$ at end of line deletes last char", () => { const state = executeVimCommands( { ...initialState, text: "hello", cursor: 4, mode: "normal" }, - ["d", "$"], + ["d", "$"] ); // Cursor at 4 (on 'o'), d$ deletes from 'o' to line end expect(state.text).toBe("hell"); @@ -384,11 +384,10 @@ describe("Vim Command Integration Tests", () => { test("x at end of text does nothing", () => { const state = executeVimCommands( { ...initialState, text: "hello", cursor: 5, mode: "normal" }, - ["x"], + ["x"] ); expect(state.text).toBe("hello"); }); - }); describe("Reported Issues", () => { @@ -398,7 +397,7 @@ describe("Vim Command Integration Tests", () => { // This caused 'ciw' to behave like 'cw' (change word forward) const state = executeVimCommands( { ...initialState, text: "hello world foo", cursor: 6, mode: "normal" }, - ["c", "i", "w"], + ["c", "i", "w"] ); expect(state.text).toBe("hello foo"); // Only "world" deleted, both spaces remain expect(state.mode).toBe("insert"); @@ -409,20 +408,19 @@ describe("Vim Command Integration Tests", () => { // In Vim: o opens new line below current line, even on last line const state = executeVimCommands( { ...initialState, text: "first\nsecond\nthird", cursor: 15, mode: "normal" }, - ["o"], + ["o"] ); expect(state.mode).toBe("insert"); expect(state.text).toBe("first\nsecond\nthird\n"); // New line added expect(state.cursor).toBe(19); // Cursor on new line }); - }); describe("e/E motion", () => { test("e moves to end of current word", () => { const state = executeVimCommands( { ...initialState, text: "hello world", cursor: 1, mode: "normal" }, - ["e"], + ["e"] ); expect(state.cursor).toBe(4); }); @@ -430,7 +428,7 @@ describe("Vim Command Integration Tests", () => { test("de deletes to end of word", () => { const state = executeVimCommands( { ...initialState, text: "hello world", cursor: 1, mode: "normal" }, - ["d", "e"], + ["d", "e"] ); expect(state.text).toBe("h world"); expect(state.yankBuffer).toBe("ello"); @@ -439,7 +437,7 @@ describe("Vim Command Integration Tests", () => { test("ce changes to end of word", () => { const state = executeVimCommands( { ...initialState, text: "hello world", cursor: 1, mode: "normal" }, - ["c", "e"], + ["c", "e"] ); expect(state.text).toBe("h world"); expect(state.mode).toBe("insert"); diff --git a/src/utils/vim.ts b/src/utils/vim.ts index 6e834f9cd..d1344cf14 100644 --- a/src/utils/vim.ts +++ b/src/utils/vim.ts @@ -123,7 +123,6 @@ export function moveWordForward(text: string, cursor: number): number { return Math.min(i, Math.max(0, n - 1)); } - /** * Move cursor to end of current/next word (like 'e'). * If on a word character, goes to end of current word. @@ -132,20 +131,20 @@ export function moveWordForward(text: string, cursor: number): number { export function moveWordEnd(text: string, cursor: number): number { const n = text.length; if (cursor >= n - 1) return Math.max(0, n - 1); - + let i = cursor; const isWord = (ch: string) => /[A-Za-z0-9_]/.test(ch); - + // If on a word char, move to end of this word if (isWord(text[i])) { while (i < n - 1 && isWord(text[i + 1])) i++; return i; } - + // If on whitespace, skip to next word then go to its end while (i < n - 1 && !isWord(text[i])) i++; while (i < n - 1 && isWord(text[i + 1])) i++; - + return Math.min(i, Math.max(0, n - 1)); } @@ -383,7 +382,7 @@ function handleInsertModeKey(state: VimState, key: string, modifiers: KeyModifie */ function handleNormalModeKey(state: VimState, key: string, modifiers: KeyModifiers): VimKeyResult { const now = Date.now(); - + // Check for timeout on pending operator (800ms like Vim) let pending = state.pendingOp; if (pending && now - pending.at > 800) { @@ -488,10 +487,7 @@ function handlePendingOperator( /** * Helper to complete an operation and clear pending state. */ -function completeOperation( - state: VimState, - updates: Partial -): VimState { +function completeOperation(state: VimState, updates: Partial): VimState { return { ...state, ...updates, @@ -612,11 +608,7 @@ function applyOperatorMotion( * Apply operator + text object combination. * Currently only supports "iw" (inner word). */ -function applyOperatorTextObject( - state: VimState, - op: "d" | "c" | "y", - textObj: "iw" -): VimState { +function applyOperatorTextObject(state: VimState, op: "d" | "c" | "y", textObj: "iw"): VimState { if (textObj !== "iw") return state; const { text, cursor, yankBuffer } = state; @@ -684,41 +676,41 @@ function tryHandleNavigation(state: VimState, key: string): VimKeyResult | null switch (key) { case "h": return handleKey(state, { cursor: Math.max(0, cursor - 1), desiredColumn: null }); - + case "l": - return handleKey(state, { - cursor: Math.min(cursor + 1, Math.max(0, text.length - 1)), - desiredColumn: null + return handleKey(state, { + cursor: Math.min(cursor + 1, Math.max(0, text.length - 1)), + desiredColumn: null, }); - + case "j": { const result = moveVertical(text, cursor, 1, desiredColumn); return handleKey(state, { cursor: result.cursor, desiredColumn: result.desiredColumn }); } - + case "k": { const result = moveVertical(text, cursor, -1, desiredColumn); return handleKey(state, { cursor: result.cursor, desiredColumn: result.desiredColumn }); } - + case "w": case "W": return handleKey(state, { cursor: moveWordForward(text, cursor), desiredColumn: null }); - + case "b": case "B": return handleKey(state, { cursor: moveWordBackward(text, cursor), desiredColumn: null }); - + case "e": case "E": return handleKey(state, { cursor: moveWordEnd(text, cursor), desiredColumn: null }); - + case "0": case "Home": { const { lineStart } = getLineBounds(text, cursor); return handleKey(state, { cursor: lineStart, desiredColumn: null }); } - + case "$": case "End": { const { lineStart, lineEnd } = getLineBounds(text, cursor); @@ -749,7 +741,7 @@ function tryHandleEdit(state: VimState, key: string): VimKeyResult | null { desiredColumn: null, }); } - + case "p": { // In normal mode, cursor is ON a character. Paste AFTER means after that character. const result = pasteAfter(text, cursor + 1, yankBuffer); @@ -759,7 +751,7 @@ function tryHandleEdit(state: VimState, key: string): VimKeyResult | null { desiredColumn: null, }); } - + case "P": { const result = pasteBefore(text, cursor, yankBuffer); return handleKey(state, { @@ -780,16 +772,16 @@ function tryHandleOperator(state: VimState, key: string, now: number): VimKeyRes switch (key) { case "d": return handleKey(state, { pendingOp: { op: "d", at: now, args: [] } }); - + case "c": return handleKey(state, { pendingOp: { op: "c", at: now, args: [] } }); - + case "y": return handleKey(state, { pendingOp: { op: "y", at: now, args: [] } }); - + case "D": return { handled: true, newState: applyOperatorMotion(state, "d", "$") }; - + case "C": return { handled: true, newState: applyOperatorMotion(state, "c", "$") }; } @@ -797,7 +789,6 @@ function tryHandleOperator(state: VimState, key: string, now: number): VimKeyRes return null; } - /** * Format pending operator command for display in mode indicator. * Returns empty string if no pending command. @@ -808,4 +799,3 @@ export function formatPendingCommand(pendingOp: VimState["pendingOp"]): string { const args = pendingOp.args?.join("") ?? ""; return `${pendingOp.op}${args}`; } - diff --git a/tests/ipcMain/anthropic1MContext.test.ts b/tests/ipcMain/anthropic1MContext.test.ts index 2425d456c..f3c0d6fcd 100644 --- a/tests/ipcMain/anthropic1MContext.test.ts +++ b/tests/ipcMain/anthropic1MContext.test.ts @@ -35,7 +35,7 @@ describeIntegration("IpcMain anthropic 1M context integration tests", () => { messageCount: 20, textPrefix: "Context test: ", }); - + // Phase 1: Try without 1M context flag - should fail with context limit error env.sentEvents.length = 0; const resultWithout1M = await sendMessageWithModel( @@ -52,23 +52,23 @@ describeIntegration("IpcMain anthropic 1M context integration tests", () => { }, } ); - + expect(resultWithout1M.success).toBe(true); - + const collectorWithout1M = createEventCollector(env.sentEvents, workspaceId); const resultType = await Promise.race([ collectorWithout1M.waitForEvent("stream-end", 30000).then(() => "success"), collectorWithout1M.waitForEvent("stream-error", 30000).then(() => "error"), ]); - + // Should get an error due to exceeding 200k token limit expect(resultType).toBe("error"); - const errorEvent = collectorWithout1M.getEvents().find( - (e) => "type" in e && e.type === "stream-error" - ) as { error: string } | undefined; + const errorEvent = collectorWithout1M + .getEvents() + .find((e) => "type" in e && e.type === "stream-error") as { error: string } | undefined; expect(errorEvent).toBeDefined(); expect(errorEvent!.error).toMatch(/too long|200000|maximum/i); - + // Phase 2: Try WITH 1M context flag // Should handle the large context better with beta header env.sentEvents.length = 0; @@ -86,18 +86,18 @@ describeIntegration("IpcMain anthropic 1M context integration tests", () => { }, } ); - + expect(resultWith1M.success).toBe(true); - + const collectorWith1M = createEventCollector(env.sentEvents, workspaceId); await collectorWith1M.waitForEvent("stream-end", 30000); - + // With 1M context, should succeed assertStreamSuccess(collectorWith1M); - + const messageWith1M = collectorWith1M.getFinalMessage(); expect(messageWith1M).toBeDefined(); - + // The key test: with 1M context, we should get a valid response // that processed the large context if (messageWith1M && "parts" in messageWith1M && Array.isArray(messageWith1M.parts)) { diff --git a/tests/ipcMain/helpers.ts b/tests/ipcMain/helpers.ts index e427fc230..6027aa4ad 100644 --- a/tests/ipcMain/helpers.ts +++ b/tests/ipcMain/helpers.ts @@ -352,11 +352,11 @@ export async function cleanupTempGitRepo(repoPath: string): Promise { /** * Build large conversation history to test context limits - * + * * This is a test-only utility that uses HistoryService directly to quickly * populate history without making API calls. Real application code should * NEVER bypass IPC like this. - * + * * @param workspaceId - Workspace to populate * @param config - Config instance for HistoryService * @param options - Configuration for history size @@ -373,26 +373,25 @@ export async function buildLargeHistory( ): Promise { const { HistoryService } = await import("../../src/services/historyService"); const { createCmuxMessage } = await import("../../src/types/message"); - + // HistoryService only needs getSessionDir, so we can cast the partial config const historyService = new HistoryService(config as any); - + const messageSize = options.messageSize ?? 50_000; const messageCount = options.messageCount ?? 80; const textPrefix = options.textPrefix ?? ""; - + const largeText = textPrefix + "A".repeat(messageSize); - + // Build conversation history with alternating user/assistant messages for (let i = 0; i < messageCount; i++) { const isUser = i % 2 === 0; const role = isUser ? "user" : "assistant"; const message = createCmuxMessage(`history-msg-${i}`, role, largeText, {}); - + const result = await historyService.appendToHistory(workspaceId, message); if (!result.success) { throw new Error(`Failed to append message ${i} to history: ${result.error}`); } } } - diff --git a/tests/ipcMain/sendMessage.test.ts b/tests/ipcMain/sendMessage.test.ts index c3ab34d12..bdbab1502 100644 --- a/tests/ipcMain/sendMessage.test.ts +++ b/tests/ipcMain/sendMessage.test.ts @@ -17,7 +17,6 @@ import { buildLargeHistory, } from "./helpers"; - // Skip all tests if TEST_INTEGRATION is not set const describeIntegration = shouldRunIntegrationTests() ? describe : describe.skip;