Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@
"node": "22.x"
},
"packageManager": "[email protected]",
"version": "2.34.3",
"version": "2.34.4",
"dependencies": {
"@dreb/coding-agent": "*",
"@mariozechner/jiti": "^2.6.5",
Expand Down
2 changes: 1 addition & 1 deletion packages/agent/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dreb/agent-core",
"version": "2.34.3",
"version": "2.34.4",
"description": "General-purpose agent with transport abstraction, state management, and attachment support",
"type": "module",
"main": "./dist/index.js",
Expand Down
2 changes: 1 addition & 1 deletion packages/ai/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dreb/ai",
"version": "2.34.3",
"version": "2.34.4",
"description": "Unified LLM API with automatic model discovery and provider configuration",
"type": "module",
"main": "./dist/index.js",
Expand Down
2 changes: 2 additions & 0 deletions packages/coding-agent/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@

### Fixed

- Fixed semantic `search` results from crashing the interactive TUI when indexed content or result previews contain CRLF or lone-CR line endings. Search result rendering and semantic-search previews now normalize platform line endings before display while preserving the strict TUI raw-line-break guard. ([#308](https://github.com/aebrer/dreb/issues/308))

- Fixed serial duplicate rows during streamed assistant/thinking markdown lists by normalizing TUI markdown render entries and rejecting raw line breaks before terminal row state can drift. ([#301](https://github.com/aebrer/dreb/issues/301))

- Fixed the TUI yanking the viewport to the top and wiping scrollback when a live-region-only change occurred while the user was scrolled up — at turn end (the working spinner disappearing) or on resize. A one-line live-region change was escalating to `recommitAll()`, which clears scrollback and repaints the whole session from the top, destroying the user's scroll position and native scrollback. The working indicator now lives on the always-present editor input line (constant live-region height, so turn boundaries no longer shrink the live region), live-region-only viewport shifts route through a new `restoreLiveViewport()` that bottom-anchors without replaying committed content, and `recommitAll()` re-establishes input modes via a RIS reset before any transcript bytes. A structural guard now makes it a loud error (with terminal teardown) for any live-only render path to reach `recommitAll()`, so the committed-replay class of bug cannot silently return. ([#292](https://github.com/aebrer/dreb/issues/292))
Expand Down
2 changes: 1 addition & 1 deletion packages/coding-agent/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dreb/coding-agent",
"version": "2.34.3",
"version": "2.34.4",
"description": "Coding agent CLI with read, bash, edit, write tools and session management",
"type": "module",
"drebConfig": {
Expand Down
6 changes: 5 additions & 1 deletion packages/coding-agent/src/core/tools/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ export interface SearchToolDetails {
indexStats?: { files: number; chunks: number };
}

function normalizeLineEndings(text: string): string {
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
}

// ============================================================================
// Rendering
// ============================================================================
Expand Down Expand Up @@ -89,7 +93,7 @@ export function formatSearchResult(
options: ToolRenderResultOptions,
theme: typeof import("../../modes/interactive/theme/theme.js").theme,
): string {
const output = result.content[0]?.text?.trim() ?? "";
const output = result.content[0]?.text ? normalizeLineEndings(result.content[0].text).trim() : "";
if (!output) return "";

const lines = output.split("\n");
Expand Down
8 changes: 4 additions & 4 deletions packages/coding-agent/test/agent-session-retry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,8 +289,8 @@ describe("AgentSession retry", () => {
// second call succeeds. This drives the _handleEvents length_retry branch
// that forwards all 5 fields to the extension runner.
let callCount = 0;
const model = findModel("anthropic", "sonnet")!;
// Guard the expected numbers against future model changes.
const model = { ...findModel("anthropic", "sonnet")!, maxTokens: 64000 };
// Keep the test fixture stable even if the live registry changes Sonnet's ceiling.
expect(model.maxTokens).toBe(64000);
const agent = new Agent({
getApiKey: () => "test-key",
Expand Down Expand Up @@ -370,8 +370,8 @@ describe("AgentSession retry", () => {
// first retry bumps 32000 → 64000 (sonnet's ceiling); a second truncation
// at the ceiling then fails loudly.
let callCount = 0;
const model = findModel("anthropic", "sonnet")!;
// Guard the expected numbers against future model changes.
const model = { ...findModel("anthropic", "sonnet")!, maxTokens: 64000 };
// Keep the test fixture stable even if the live registry changes Sonnet's ceiling.
expect(model.maxTokens).toBe(64000);
const agent = new Agent({
getApiKey: () => "test-key",
Expand Down
13 changes: 13 additions & 0 deletions packages/coding-agent/test/search/search-tool.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,19 @@ describe("createSearchToolDefinition", () => {
expect(result).not.toContain("more lines");
});

it("normalizes CRLF and lone CR result text", () => {
const result = formatSearchResult(
{ content: [{ type: "text", text: "first\r\nsecond\rthird" }] },
{ expanded: false, isPartial: false },
stubTheme,
);

expect(result).toContain("first");
expect(result).toContain("second");
expect(result).toContain("third");
expect(result).not.toContain("\r");
});

it("truncates at 20 lines in collapsed mode with remaining count", () => {
const lines = Array.from({ length: 30 }, (_, i) => `line ${i + 1}`);
const result = formatSearchResult(
Expand Down
2 changes: 1 addition & 1 deletion packages/semantic-search/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "semantic-search",
"description": "Semantic codebase search — natural language queries over code and docs using embeddings, tree-sitter parsing, and POEM multi-signal ranking",
"version": "2.34.3",
"version": "2.34.4",
"author": {
"name": "Drew Brereton"
},
Expand Down
2 changes: 1 addition & 1 deletion packages/semantic-search/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dreb/semantic-search",
"version": "2.34.3",
"version": "2.34.4",
"description": "Semantic codebase search engine with embedding-based ranking and MCP server",
"publishConfig": {
"access": "public"
Expand Down
6 changes: 5 additions & 1 deletion packages/semantic-search/src/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@

import type { SearchResult } from "./types.js";

function splitLogicalLines(text: string): string[] {
return text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").split("\n");
}

export function formatResults(results: SearchResult[]): string {
if (results.length === 0) return "No results found.";

Expand All @@ -28,7 +32,7 @@ export function formatResults(results: SearchResult[]): string {
}

// Content preview (first 3 lines)
const contentLines = chunk.content.split("\n");
const contentLines = splitLogicalLines(chunk.content);
const previewLines = contentLines.slice(0, 3);
for (const line of previewLines) {
const trimmed = line.length > 120 ? `${line.slice(0, 117)}...` : line;
Expand Down
13 changes: 13 additions & 0 deletions packages/semantic-search/test/mcp-server.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,19 @@ describe("formatResults", () => {
expect(output).toContain("... (2 more lines)");
});

it("normalizes CRLF and lone CR content previews", () => {
const content = "line 1\r\nline 2\rline 3\r\nline 4\r\nline 5";
const result = makeResult({ chunk: makeChunk({ content }) });
const output = formatResults([result]);

expect(output).toContain("line 1");
expect(output).toContain("line 2");
expect(output).toContain("line 3");
expect(output).not.toContain("line 4");
expect(output).toContain("... (2 more lines)");
expect(output).not.toContain("\r");
});

it("truncates long content lines at 120 chars", () => {
const longLine = "x".repeat(200);
const result = makeResult({ chunk: makeChunk({ content: longLine }) });
Expand Down
2 changes: 1 addition & 1 deletion packages/telegram/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dreb/telegram",
"version": "2.34.3",
"version": "2.34.4",
"description": "Telegram bot frontend for dreb coding agent",
"license": "MIT",
"type": "module",
Expand Down
1 change: 1 addition & 0 deletions packages/tui/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

### Fixed

- Fixed `Text` rendering for CRLF and lone-CR input so platform line endings are split into separate logical render entries instead of returning raw carriage returns to the TUI guard. ([#308](https://github.com/aebrer/dreb/issues/308))
- Fixed streamed markdown list rendering from returning raw newline characters inside a single TUI render entry, preventing serial duplicate rows during soft-wrapped assistant output ([#301](https://github.com/aebrer/dreb/issues/301))
- Fixed the TUI render-contract guard so raw line breaks are rejected even when a rendered entry contains terminal image escape sequences
- Fixed blockquote text color breaking after inline links (and other inline elements) due to missing style restoration prefix
Expand Down
2 changes: 1 addition & 1 deletion packages/tui/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@dreb/tui",
"version": "2.34.3",
"version": "2.34.4",
"description": "Terminal User Interface library with differential rendering for efficient text-based applications",
"type": "module",
"main": "dist/index.js",
Expand Down
4 changes: 2 additions & 2 deletions packages/tui/src/components/text.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ export class Text implements Component {
return result;
}

// Replace tabs with 3 spaces
const normalizedText = this.text.replace(/\t/g, " ");
// Normalize line endings and replace tabs with 3 spaces.
const normalizedText = this.text.replace(/\r\n/g, "\n").replace(/\r/g, "\n").replace(/\t/g, " ");

// Add top/bottom padding (empty lines)
const emptyLine = " ".repeat(width);
Expand Down
23 changes: 23 additions & 0 deletions packages/tui/test/markdown-softwrap.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,29 @@ describe("Text softWrap", () => {
assert.strictEqual(lines[2], " ".repeat(20));
});

it("normalizes CRLF and lone CR input into separate soft-wrapped logical lines", () => {
const text = new Text("first\r\nsecond\rthird", 0, 0, undefined, true);

const lines = text.render(20);
const plainLines = lines.map(plain);

assertNoRawLineBreaks(lines);
assert.deepStrictEqual(plainLines, ["first", "second", "third"]);
assert.ok(lines.every(isWrappableLine));
});

it("normalizes CRLF and lone CR input into separate hard-wrapped logical lines", () => {
const text = new Text("first\r\nsecond\rthird", 0, 0);

const lines = text.render(20);
const plainLines = lines.map((line) => plain(line).trimEnd());

assertNoRawLineBreaks(lines);
assert.deepStrictEqual(plainLines, ["first", "second", "third"]);
assert.strictEqual(lines.some(isWrappableLine), false);
assert.ok(lines.every((line) => visibleWidth(line) === 20));
});

it("defaults to existing padded hard-wrapped behavior", () => {
const text = new Text("short", 2, 0);

Expand Down
Loading