fix: fixed LSP server display, file streaming, and slash command autocomplete; reorganized npm scripts
- Fixed LSP servers showing as 'unknown' in status display when server warmup fails by using index-based lookup to correctly extract server name and file types. - Fixed Read tool loading entire file into memory when offset/limit was specified by implementing streaming line-by-line processing that only collects requested range. - Fixed slash command autocomplete applying stale completions when typing quickly by adding staleness check before applying completion on Enter/Return. - Reorganized npm scripts to follow consistent naming pattern with separate TypeScript and Rust variants for check, lint, fmt, and fix operations. - Updated release script to properly handle Rust workspace version updates in Cargo.toml files and regenerate lockfiles.
This commit is contained in:
+12
-7
@@ -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",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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<typeof readSchema, ReadToolDetails> {
|
||||
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<ReadToolDetails>()
|
||||
.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<typeof readSchema, ReadToolDetails> {
|
||||
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<typeof readSchema, ReadToolDetails> {
|
||||
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
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Fixed
|
||||
- Fixed slash command autocomplete applying stale completion when typing quickly
|
||||
|
||||
## [8.4.1] - 2026-01-25
|
||||
|
||||
### Added
|
||||
|
||||
@@ -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
|
||||
|
||||
+34
-9
@@ -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<void> {
|
||||
}
|
||||
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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user