fix: stabilize merged review follow-ups
This commit is contained in:
@@ -6,6 +6,8 @@
|
||||
|
||||
- Fixed `local://` URL path leak on Linux where `//` collapsing to `/` produced `local:/path` forms that bypassed the internal protocol handler and leaked as filesystem paths, breaking plan mode file resolution
|
||||
- Fixed Tavily web search silently returning off-topic news articles when `--recency` was set. The provider was unconditionally coupling `topic: "news"` to recency, which scoped Tavily's index to news publications and excluded documentation, release notes, GitHub, and all non-news technical content. Technical queries with `--recency` now return the correct corpus.
|
||||
- Fixed status-line sanitization to strip OSC, DCS, PM, APC, and 8-bit CSI escape sequences instead of leaving payload fragments in the UI
|
||||
- Fixed read tool previews to render content by default without paying full-file highlight cost in collapsed mode, while preserving warning styling for corrected paths
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -761,9 +761,9 @@ export async function computeEditDiff(
|
||||
if (oldText.length === 0) {
|
||||
return { error: "oldText must not be empty." };
|
||||
}
|
||||
const absolutePath = resolveToCwd(path, cwd);
|
||||
|
||||
try {
|
||||
const absolutePath = resolveToCwd(path, cwd);
|
||||
let rawContent: string;
|
||||
try {
|
||||
rawContent = await readFileTextForDiff(path, absolutePath);
|
||||
|
||||
@@ -1157,11 +1157,11 @@ export async function computeHashlineDiff(
|
||||
}
|
||||
> {
|
||||
const { path, edits, move } = input;
|
||||
const absolutePath = resolveToCwd(path, cwd);
|
||||
const movePath = move ? resolveToCwd(move, cwd) : undefined;
|
||||
const isMoveOnly = Boolean(movePath) && movePath !== absolutePath && edits.length === 0;
|
||||
|
||||
try {
|
||||
const absolutePath = resolveToCwd(path, cwd);
|
||||
const movePath = move ? resolveToCwd(move, cwd) : undefined;
|
||||
const isMoveOnly = Boolean(movePath) && movePath !== absolutePath && edits.length === 0;
|
||||
const resolvedEdits = resolveHashlineEditsForDiff(edits);
|
||||
const file = Bun.file(absolutePath);
|
||||
|
||||
|
||||
@@ -176,7 +176,7 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
|
||||
code: entry.contentText ?? "",
|
||||
language: lang,
|
||||
title,
|
||||
status: entry.status === "error" ? "error" : entry.status === "pending" ? "pending" : "complete",
|
||||
status: entry.status === "success" ? "complete" : entry.status,
|
||||
expanded,
|
||||
codeMaxLines: expanded ? undefined : COLLAPSED_PREVIEW_LINES,
|
||||
width,
|
||||
|
||||
@@ -28,7 +28,6 @@ import type {
|
||||
import type { CompactOptions } from "../extensibility/extensions/types";
|
||||
import { BUILTIN_SLASH_COMMANDS, loadSlashCommands } from "../extensibility/slash-commands";
|
||||
import { resolveLocalUrlToPath } from "../internal-urls";
|
||||
import { normalizeLocalScheme } from "../tools/path-utils";
|
||||
import { LSP_STARTUP_EVENT_CHANNEL, type LspStartupEvent } from "../lsp/startup-events";
|
||||
import { renameApprovedPlanFile } from "../plan-mode/approved-plan";
|
||||
import planModeApprovedPrompt from "../prompts/system/plan-mode-approved.md" with { type: "text" };
|
||||
@@ -38,6 +37,7 @@ import type { SessionContext, SessionManager } from "../session/session-manager"
|
||||
import { getRecentSessions } from "../session/session-manager";
|
||||
import { STTController, type SttState } from "../stt";
|
||||
import type { ExitPlanModeDetails, LspStartupServerInfo } from "../tools";
|
||||
import { normalizeLocalScheme } from "../tools/path-utils";
|
||||
import type { EventBus } from "../utils/event-bus";
|
||||
import { getEditorCommand, openInEditor } from "../utils/external-editor";
|
||||
import { getSessionAccentAnsi, getSessionAccentHexForTitle } from "../utils/session-color";
|
||||
|
||||
@@ -5,14 +5,16 @@ import { theme } from "./theme/theme";
|
||||
// Text Sanitization
|
||||
// ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
const ANSI_OSC_RE = /\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)/g;
|
||||
const ANSI_CSI_RE = /\x1b\[[0-?]*[ -/]*[@-~]/g;
|
||||
const ANSI_OSC_RE = /\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x9d[^\x07\x9c]*(?:\x07|\x9c)/g;
|
||||
const ANSI_STRING_RE = /\x1b(?:P|_|\^)[\s\S]*?\x1b\\|[\x90\x9e\x9f][\s\S]*?\x9c/g;
|
||||
const ANSI_CSI_RE = /\x1b\[[0-?]*[ -/]*[@-~]|\x9b[0-?]*[ -/]*[@-~]/g;
|
||||
const ANSI_SINGLE_RE = /\x1b[@-Z\\-_]/g;
|
||||
|
||||
/** Sanitize text for display in a single-line status. Strips ANSI escape sequences, C0/C1 control characters, collapses whitespace, trims. */
|
||||
export function sanitizeStatusText(text: string): string {
|
||||
return text
|
||||
.replace(ANSI_OSC_RE, "")
|
||||
.replace(ANSI_STRING_RE, "")
|
||||
.replace(ANSI_CSI_RE, "")
|
||||
.replace(ANSI_SINGLE_RE, "")
|
||||
.replace(/[\u0000-\u001f\u007f-\u009f]/g, " ")
|
||||
|
||||
@@ -18,7 +18,7 @@ export interface CodeCellOptions {
|
||||
index?: number;
|
||||
total?: number;
|
||||
title?: string;
|
||||
status?: "pending" | "running" | "complete" | "error";
|
||||
status?: "pending" | "running" | "warning" | "complete" | "error";
|
||||
spinnerFrame?: number;
|
||||
duration?: number;
|
||||
output?: string;
|
||||
@@ -32,6 +32,7 @@ function getState(status?: CodeCellOptions["status"]): State | undefined {
|
||||
if (!status) return undefined;
|
||||
if (status === "complete") return "success";
|
||||
if (status === "error") return "error";
|
||||
if (status === "warning") return "warning";
|
||||
if (status === "running") return "running";
|
||||
return "pending";
|
||||
}
|
||||
@@ -45,9 +46,11 @@ function formatHeader(options: CodeCellOptions, theme: Theme): { title: string;
|
||||
? "success"
|
||||
: status === "error"
|
||||
? "error"
|
||||
: status === "running"
|
||||
? "running"
|
||||
: "pending",
|
||||
: status === "warning"
|
||||
? "warning"
|
||||
: status === "running"
|
||||
? "running"
|
||||
: "pending",
|
||||
theme,
|
||||
spinnerFrame,
|
||||
);
|
||||
@@ -78,10 +81,12 @@ export function renderCodeCell(options: CodeCellOptions, theme: Theme): string[]
|
||||
const { title, meta } = formatHeader(options, theme);
|
||||
const state = getState(options.status);
|
||||
|
||||
const rawCodeLines = highlightCode(replaceTabs(code ?? ""), language);
|
||||
const normalizedCode = replaceTabs(code ?? "");
|
||||
const rawCodeLines = normalizedCode.split("\n");
|
||||
const maxCodeLines = expanded ? rawCodeLines.length : Math.min(rawCodeLines.length, codeMaxLines);
|
||||
const codeLines = rawCodeLines.slice(0, maxCodeLines);
|
||||
const hiddenCodeLines = rawCodeLines.length - codeLines.length;
|
||||
const visibleCode = rawCodeLines.slice(0, maxCodeLines).join("\n");
|
||||
const codeLines = highlightCode(visibleCode, language);
|
||||
const hiddenCodeLines = rawCodeLines.length - maxCodeLines;
|
||||
if (hiddenCodeLines > 0) {
|
||||
const hint = formatExpandHint(theme, expanded, hiddenCodeLines > 0);
|
||||
const moreLine = `${formatMoreItems(hiddenCodeLines, "line")}${hint ? ` ${hint}` : ""}`;
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as os from "node:os";
|
||||
import * as path from "node:path";
|
||||
import {
|
||||
adjustIndentation,
|
||||
computeEditDiff,
|
||||
computeHashlineDiff,
|
||||
DEFAULT_FUZZY_THRESHOLD,
|
||||
findMatch,
|
||||
@@ -269,4 +270,36 @@ describe("computeHashlineDiff", () => {
|
||||
expect(result.firstChangedLine).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
test("returns a handled error when the source path is a local URL", async () => {
|
||||
const result = await computeHashlineDiff({ path: "local://PLAN.md", edits: [] }, tempDir);
|
||||
|
||||
expect("error" in result).toBe(true);
|
||||
if ("error" in result) {
|
||||
expect(result.error).toContain('internal scheme "local://"');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe("computeEditDiff", () => {
|
||||
let tempDir = "";
|
||||
|
||||
beforeEach(async () => {
|
||||
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "edit-diff-edit-"));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
if (tempDir) {
|
||||
await fs.rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("returns a handled error when the source path is a local URL", async () => {
|
||||
const result = await computeEditDiff("local:/PLAN.md", "old", "new", tempDir);
|
||||
|
||||
expect("error" in result).toBe(true);
|
||||
if ("error" in result) {
|
||||
expect(result.error).toContain('internal scheme "local://"');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
import { afterEach, beforeAll, describe, expect, it, vi } from "bun:test";
|
||||
import { ReadToolGroupComponent } from "../src/modes/components/read-tool-group";
|
||||
import * as themeModule from "../src/modes/theme/theme";
|
||||
|
||||
describe("ReadToolGroupComponent", () => {
|
||||
beforeAll(async () => {
|
||||
await themeModule.initTheme(false, undefined, undefined, "dark", "light");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it("renders warning previews with warning styling instead of success styling", () => {
|
||||
const component = new ReadToolGroupComponent();
|
||||
component.updateArgs({ path: "/tmp/example.ts" }, "read-1");
|
||||
component.updateResult(
|
||||
{
|
||||
content: [{ type: "text", text: "const a = 1;\nconst b = 2;\nconst c = 3;" }],
|
||||
details: { suffixResolution: { from: "/tmp/exampl.ts", to: "/tmp/example.ts" } },
|
||||
},
|
||||
false,
|
||||
"read-1",
|
||||
);
|
||||
|
||||
const rendered = Bun.stripANSI(component.render(120).join("\n"));
|
||||
|
||||
expect(rendered).toContain(themeModule.theme.status.warning);
|
||||
expect(rendered).not.toContain(themeModule.theme.status.success);
|
||||
expect(rendered).toContain("corrected from");
|
||||
});
|
||||
|
||||
it("highlights only the collapsed preview lines", () => {
|
||||
const highlightSpy = vi.spyOn(themeModule, "highlightCode");
|
||||
const component = new ReadToolGroupComponent();
|
||||
component.updateArgs({ path: "/tmp/example.ts" }, "read-2");
|
||||
component.updateResult(
|
||||
{
|
||||
content: [
|
||||
{
|
||||
type: "text",
|
||||
text: "line 1\nline 2\nline 3\nline 4\nline 5",
|
||||
},
|
||||
],
|
||||
},
|
||||
false,
|
||||
"read-2",
|
||||
);
|
||||
|
||||
const rendered = Bun.stripANSI(component.render(120).join("\n"));
|
||||
const highlightedInput = highlightSpy.mock.calls[0]?.[0];
|
||||
|
||||
expect(highlightedInput).toBe("line 1\nline 2\nline 3");
|
||||
expect(rendered).toContain("line 1");
|
||||
expect(rendered).not.toContain("line 4");
|
||||
expect(rendered.toLowerCase()).toContain("ctrl+o");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
import { describe, expect, it } from "bun:test";
|
||||
import { sanitizeStatusText } from "../src/modes/shared";
|
||||
|
||||
describe("sanitizeStatusText", () => {
|
||||
it("strips OSC, DCS, PM, APC, and 8-bit CSI escape sequences", () => {
|
||||
const input =
|
||||
"prefix " +
|
||||
"\x1b]8;;https://example.com\x07link\x1b]8;;\x07" +
|
||||
" " +
|
||||
"\x1bPhidden-dcs\x1b\\" +
|
||||
"\x1b^hidden-pm\x1b\\" +
|
||||
"\x1b_hidden-apc\x1b\\" +
|
||||
"\x9b31mred\x9b0m" +
|
||||
" suffix";
|
||||
|
||||
expect(sanitizeStatusText(input)).toBe("prefix link red suffix");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user