diff --git a/package.json b/package.json index 50677522f..78ea3bab9 100644 --- a/package.json +++ b/package.json @@ -9,14 +9,19 @@ "install:dev": "bun install && bun --cwd=packages/coding-agent link && bun --cwd=packages/ai link", "dev": "bun packages/coding-agent/src/cli.ts", "stats": "bun --cwd=packages/coding-agent src/cli.ts stats", - "check": "biome check --write . && tsgo -p tsconfig.json && tsgo -p packages/stats/tsconfig.client.json", "test": "bun --cwd=packages/ai run test && bun --cwd=packages/agent run test && bun --cwd=packages/tui run test && bun --cwd=packages/natives run test && bun --cwd=packages/coding-agent run test", - "fix": "biome check --write --unsafe . && bun --cwd=packages/coding-agent run format-prompts", - "rust:check": "cargo check --workspace", - "rust:clippy": "cargo clippy --workspace -- -D warnings", - "rust:fmt": "cargo fmt --all -- --check", - "rust:fmt:fix": "cargo fmt --all", - "rust:lint": "cargo fmt --all -- --check && cargo clippy --workspace -- -D warnings", + "check": "bun run check:ts && bun run check:rs", + "check:ts": "biome check . && tsgo -p tsconfig.json && tsgo -p packages/stats/tsconfig.client.json", + "check:rs": "cargo fmt --all -- --check && cargo clippy --workspace -- -D warnings", + "lint": "bun run lint:ts && bun run lint:rs", + "lint:ts": "biome lint .", + "lint:rs": "cargo clippy --workspace -- -D warnings", + "fmt": "bun run fmt:ts && bun run fmt:rs", + "fmt:ts": "biome format --write .", + "fmt:rs": "cargo fmt --all", + "fix": "bun run fix:ts && bun run fix:rs", + "fix:ts": "biome check --write --unsafe . && bun --cwd=packages/coding-agent run format-prompts", + "fix:rs": "cargo clippy --fix --allow-dirty --all-targets --no-deps --allow-staged --broken-code --allow-no-vcs && cargo fmt --all", "bench:gen-fixtures": "bun run packages/react-edit-benchmark/generate.ts --react-dir /tmp/react-source --count-per-type 3", "bench:edit": "bun --cwd=packages/react-edit-benchmark run start", "version:patch": "npm version patch -ws --no-git-tag-version && bun scripts/sync-versions.ts && rm -rf node_modules packages/*/node_modules bun.lock && bun install", diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index f177d4d59..87a2704f8 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed +- Fixed LSP servers showing as "unknown" in status display when server warmup fails +- Fixed Read tool loading entire file into memory when offset/limit was specified + ## [8.12.2] - 2026-01-28 ### Changed diff --git a/packages/coding-agent/src/lsp/index.ts b/packages/coding-agent/src/lsp/index.ts index 8960db4e8..4be7918e5 100644 --- a/packages/coding-agent/src/lsp/index.ts +++ b/packages/coding-agent/src/lsp/index.ts @@ -107,7 +107,9 @@ export async function warmupLspServers(cwd: string, options?: LspWarmupOptions): }), ); - for (const result of results) { + for (let i = 0; i < results.length; i++) { + const result = results[i]; + const [name, serverConfig] = lspServers[i]; if (result.status === "fulfilled") { servers.push({ name: result.value.name, @@ -115,12 +117,11 @@ export async function warmupLspServers(cwd: string, options?: LspWarmupOptions): fileTypes: result.value.fileTypes, }); } else { - // Extract server name from error if possible const errorMsg = result.reason?.message ?? String(result.reason); servers.push({ - name: "unknown", + name, status: "error", - fileTypes: [], + fileTypes: serverConfig.fileTypes, error: errorMsg, }); } diff --git a/packages/coding-agent/src/tools/read.ts b/packages/coding-agent/src/tools/read.ts index 69ce82f60..ab7d45b21 100644 --- a/packages/coding-agent/src/tools/read.ts +++ b/packages/coding-agent/src/tools/read.ts @@ -43,6 +43,87 @@ function isRemoteMountPath(absolutePath: string): boolean { return absolutePath.startsWith(REMOTE_MOUNT_PREFIX); } +/** + * Stream lines from a file, collecting only the requested range. + * Avoids loading the entire file into memory for large files. + * + * @param filePath - Path to the file + * @param startLine - 0-indexed start line + * @param maxLinesToCollect - Maximum lines to collect (from startLine) + * @param maxBytes - Maximum bytes to collect + * @returns Collected lines, total line count, and truncation info + */ +async function streamLinesFromFile( + filePath: string, + startLine: number, + maxLinesToCollect: number, + maxBytes: number, +): Promise<{ + lines: string[]; + totalFileLines: number; + collectedBytes: number; + stoppedByByteLimit: boolean; +}> { + const stream = Bun.file(filePath).stream(); + const decoder = new TextDecoder(); + + const collectedLines: string[] = []; + let lineIndex = 0; + let collectedBytes = 0; + let stoppedByByteLimit = false; + let buffer = ""; + let doneCollecting = false; + + for await (const chunk of stream) { + buffer += decoder.decode(chunk, { stream: true }); + + for (let newlinePos = buffer.indexOf("\n"); newlinePos !== -1; newlinePos = buffer.indexOf("\n")) { + const line = buffer.slice(0, newlinePos); + buffer = buffer.slice(newlinePos + 1); + + if (!doneCollecting && lineIndex >= startLine) { + const lineBytes = Buffer.byteLength(line, "utf-8") + (collectedLines.length > 0 ? 1 : 0); + + if (collectedBytes + lineBytes > maxBytes && collectedLines.length > 0) { + stoppedByByteLimit = true; + doneCollecting = true; + } else if (collectedLines.length < maxLinesToCollect) { + collectedLines.push(line); + collectedBytes += lineBytes; + if (collectedLines.length >= maxLinesToCollect) { + doneCollecting = true; + } + } else { + doneCollecting = true; + } + } + + lineIndex++; + } + } + + // Handle remaining buffer (last line without trailing newline) + if (buffer.length > 0) { + if (!doneCollecting && lineIndex >= startLine && collectedLines.length < maxLinesToCollect) { + const lineBytes = Buffer.byteLength(buffer, "utf-8") + (collectedLines.length > 0 ? 1 : 0); + if (collectedBytes + lineBytes <= maxBytes || collectedLines.length === 0) { + collectedLines.push(buffer); + collectedBytes += lineBytes; + } else { + stoppedByByteLimit = true; + } + } + lineIndex++; + } + + return { + lines: collectedLines, + totalFileLines: lineIndex, + collectedBytes, + stoppedByByteLimit, + }; +} + // Maximum image file size (20MB) - larger images will be rejected to prevent OOM during serialization const MAX_IMAGE_SIZE = 20 * 1024 * 1024; const MAX_FUZZY_RESULTS = 5; @@ -514,40 +595,51 @@ export class ReadTool implements AgentTool { content = [{ type: "text", text: `[Cannot read ${ext} file: conversion failed]` }]; } } else { - // Read as text - const file = Bun.file(absolutePath); - const textContent = await file.text(); - const allLines = textContent.split("\n"); - const totalFileLines = allLines.length; - - // Apply offset if specified (1-indexed to 0-indexed) + // Read as text using streaming to avoid loading huge files into memory const startLine = offset ? Math.max(0, offset - 1) : 0; const startLineDisplay = startLine + 1; // For display (1-indexed) + // Calculate how many lines to collect: user limit or default truncation limit + const maxLinesToCollect = limit !== undefined ? limit : DEFAULT_MAX_LINES; + + // Stream the file, collecting only the needed lines + const streamResult = await streamLinesFromFile(absolutePath, startLine, maxLinesToCollect, DEFAULT_MAX_BYTES); + + const { lines: collectedLines, totalFileLines, collectedBytes, stoppedByByteLimit } = streamResult; + // Check if offset is out of bounds - return graceful message instead of throwing - if (startLine >= allLines.length) { + if (startLine >= totalFileLines) { const suggestion = - allLines.length === 0 + totalFileLines === 0 ? "The file is empty." - : `Use offset=1 to read from the start, or offset=${allLines.length} to read the last line.`; + : `Use offset=1 to read from the start, or offset=${totalFileLines} to read the last line.`; return toolResult() - .text(`Offset ${offset} is beyond end of file (${allLines.length} lines total). ${suggestion}`) + .text(`Offset ${offset} is beyond end of file (${totalFileLines} lines total). ${suggestion}`) .done(); } - // If limit is specified by user, use it; otherwise we'll let truncateHead decide - let selectedContent: string; - let userLimitedLines: number | undefined; - if (limit !== undefined) { - const endLine = Math.min(startLine + limit, allLines.length); - selectedContent = allLines.slice(startLine, endLine).join("\n"); - userLimitedLines = endLine - startLine; - } else { - selectedContent = allLines.slice(startLine).join("\n"); - } + // Build the selected content from collected lines + const selectedContent = collectedLines.join("\n"); + const userLimitedLines = limit !== undefined ? collectedLines.length : undefined; - // Apply truncation (respects both line and byte limits) - const truncation = truncateHead(selectedContent); + // Build truncation result from streaming data + const totalSelectedLines = totalFileLines - startLine; + const totalSelectedBytes = collectedBytes; // We don't know exact total bytes without reading all + const wasTruncated = collectedLines.length < totalSelectedLines || stoppedByByteLimit; + + const truncation: TruncationResult = { + content: selectedContent, + truncated: wasTruncated, + truncatedBy: stoppedByByteLimit ? "bytes" : wasTruncated ? "lines" : null, + totalLines: totalSelectedLines, + totalBytes: totalSelectedBytes, + outputLines: collectedLines.length, + outputBytes: collectedBytes, + lastLinePartial: false, + firstLineExceedsLimit: collectedLines.length === 0 && totalFileLines > startLine, + maxLines: DEFAULT_MAX_LINES, + maxBytes: DEFAULT_MAX_BYTES, + }; // Add line numbers if requested (uses setting default if not specified) const shouldAddLineNumbers = lines ?? this.defaultLineNumbers; @@ -566,7 +658,7 @@ export class ReadTool implements AgentTool { let outputText: string; if (truncation.firstLineExceedsLimit) { - const firstLine = allLines[startLine] ?? ""; + const firstLine = collectedLines[0] ?? ""; const firstLineBytes = Buffer.byteLength(firstLine, "utf-8"); const snippet = truncateStringToBytesFromStart(firstLine, DEFAULT_MAX_BYTES); @@ -592,8 +684,8 @@ export class ReadTool implements AgentTool { result: truncation, options: { direction: "head", startLine: startLineDisplay, totalFileLines }, }; - } else if (userLimitedLines !== undefined && startLine + userLimitedLines < allLines.length) { - const remaining = allLines.length - (startLine + userLimitedLines); + } else if (userLimitedLines !== undefined && startLine + userLimitedLines < totalFileLines) { + const remaining = totalFileLines - (startLine + userLimitedLines); const nextOffset = startLine + userLimitedLines + 1; outputText = shouldAddLineNumbers diff --git a/packages/tui/CHANGELOG.md b/packages/tui/CHANGELOG.md index 6fe991d3b..54f566a83 100644 --- a/packages/tui/CHANGELOG.md +++ b/packages/tui/CHANGELOG.md @@ -2,6 +2,9 @@ ## [Unreleased] +### Fixed +- Fixed slash command autocomplete applying stale completion when typing quickly + ## [8.4.1] - 2026-01-25 ### Added diff --git a/packages/tui/src/components/editor.ts b/packages/tui/src/components/editor.ts index dc1250c45..e474e0e8f 100644 --- a/packages/tui/src/components/editor.ts +++ b/packages/tui/src/components/editor.ts @@ -743,21 +743,29 @@ export class Editor implements Component, Focusable { (matchesKey(data, "enter") || matchesKey(data, "return") || data === "\n") && this.autocompletePrefix.startsWith("/") ) { - const selected = this.autocompleteList.getSelectedItem(); - if (selected && this.autocompleteProvider) { - const result = this.autocompleteProvider.applyCompletion( - this.state.lines, - this.state.cursorLine, - this.state.cursorCol, - selected, - this.autocompletePrefix, - ); + // Check for stale autocomplete state due to debounce + const currentLine = this.state.lines[this.state.cursorLine] ?? ""; + const currentTextBeforeCursor = currentLine.slice(0, this.state.cursorCol); + if (currentTextBeforeCursor !== this.autocompletePrefix) { + // Autocomplete is stale - cancel and fall through to normal submission + this.cancelAutocomplete(); + } else { + const selected = this.autocompleteList.getSelectedItem(); + if (selected && this.autocompleteProvider) { + const result = this.autocompleteProvider.applyCompletion( + this.state.lines, + this.state.cursorLine, + this.state.cursorCol, + selected, + this.autocompletePrefix, + ); - this.state.lines = result.lines; - this.state.cursorLine = result.cursorLine; - this.state.cursorCol = result.cursorCol; + this.state.lines = result.lines; + this.state.cursorLine = result.cursorLine; + this.state.cursorCol = result.cursorCol; + } + this.cancelAutocomplete(); } - this.cancelAutocomplete(); // Don't return - fall through to submission logic } // If Enter was pressed on a file path, apply completion diff --git a/scripts/release.ts b/scripts/release.ts index c07a2f86c..ecc724569 100755 --- a/scripts/release.ts +++ b/scripts/release.ts @@ -13,6 +13,7 @@ import { $, Glob } from "bun"; const changelogGlob = new Glob("packages/*/CHANGELOG.md"); const packageJsonGlob = new Glob("packages/*/package.json"); +const cargoTomlGlob = new Glob("crates/*/Cargo.toml"); // ============================================================================= // Shared functions @@ -170,36 +171,60 @@ async function cmdRelease(version: string): Promise { } console.log(); - // 3. Regenerate lockfile - console.log("Regenerating lockfile..."); - await $`rm -f bun.lock`; - await $`bun install`; + // 3. Update Rust workspace version + console.log(`Updating Rust workspace version to ${version}...`); + await $`sd '^version = "[^"]+"' ${`version = "${version}"`} Cargo.toml`; + + // Verify + const cargoToml = await Bun.file("Cargo.toml").text(); + const versionMatch = cargoToml.match(/^\[workspace\.package\][\s\S]*?^version = "([^"]+)"/m); + if (versionMatch) { + console.log(` workspace: ${versionMatch[1]}`); + } + + // List crates using workspace version + for await (const cargoPath of cargoTomlGlob.scan(".")) { + const content = await Bun.file(cargoPath).text(); + if (content.includes("version.workspace = true")) { + const nameMatch = content.match(/^name = "([^"]+)"/m); + if (nameMatch) { + console.log(` ${nameMatch[1]}: ${version} (workspace)`); + } + } + } console.log(); - // 4. Update changelogs + // 4. Regenerate lockfiles + console.log("Regenerating lockfiles..."); + await $`rm -f bun.lock`; + await $`bun install`; + await $`cargo generate-lockfile`; + console.log(); + + // 5. Update changelogs console.log("Updating CHANGELOGs..."); await updateChangelogsForRelease(version); console.log(); - // 5. Run checks + // 6. Run checks console.log("Running checks..."); await $`bun run check`; console.log(); - // 6. Commit and tag + // 7. Commit and tag console.log("Committing and tagging..."); await $`git add .`; await $`git commit -m ${`chore: bump version to ${version}`}`; await $`git tag ${`v${version}`}`; console.log(); - // 7. Push + // 8. Push console.log("Pushing to remote..."); await $`git push origin main`; await $`git push origin ${`v${version}`}`; console.log(); - // 8. Watch CI + // 9. Watch CI console.log("Watching CI..."); const success = await watchCI();