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
4 changes: 2 additions & 2 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,8 @@ Only update the `Status` field. Do not modify any other frontmatter or prompt co

<!-- BEGIN:REPO:current-state -->
## Current State
Branch: `feature/version-1.0.0-alpha.72`
In-progress: PR shellicar/claude-cli#160 open, auto-merge enabled. Awaiting CodeQL and merge. Phase 2 (github-release) to follow after merge.
Branch: `main`
In-progress: PR shellicar/claude-cli#162 open, auto-merge enabled. Awaiting CodeQL and merge.
<!-- END:REPO:current-state -->

<!-- BEGIN:REPO:architecture -->
Expand Down
28 changes: 28 additions & 0 deletions .claude/sessions/2026-03-30.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,31 @@
### 07:15 - fix/rendering-duplication Phase 2 (#161)

- Did: Pushed `fix/161-rendering-duplication` branch; created PR shellicar/claude-cli#162 with `Closes #161`, milestone `1.0`, label `bug`, reviewer `bananabot9000`, auto-merge enabled. Build and signature checks passed; CodeQL in progress.
- Files: (no code changes)
- Decisions: None.
- Next: Nothing. PR will auto-merge when CodeQL passes.

### 07:01 - fix/rendering-duplication Phase 1 (#161)

- Did: Fixed three rendering bugs: (1) split embedded newlines in `Terminal.writeHistory()` so each segment is its own `displayBuffer` / `historyBuffer` entry; (2) added `this.term.isHistoryMode` guard before the editor block in `handleKey()` to block editor input during history mode; (3) removed redundant `this.term.refresh()` from the `appState.on('changed')` handler, leaving only `this.redraw()`. Added 6 tests in `test/terminal.spec.ts` covering multi-line split and edge cases. Added `testDisplayBuffer` getter to `Terminal` for test access. All 230 tests pass.
- Files: `src/terminal.ts`, `src/ClaudeCli.ts`, `test/terminal.spec.ts`
- Decisions: `testDisplayBuffer` getter added to Terminal to expose private `displayBuffer` for tests without changing production access patterns. Tests mock `process.stdout.write` to suppress noise from the non-alt-buffer screen write path. History mode guard test skipped (not testable without a ClaudeCli harness, per prompt qualifier).
- Next: Phase 2 (push branch + PR with Closes #161)

### 06:50 - investigation/rendering-duplication (#161)

- Did: Investigated rendering duplication symptoms. Read all core rendering files (TerminalRenderer, terminal, Layout, Viewport, Screen, HistoryViewport, ClaudeCli, AppState, renderer). Identified 9 bugs across the rendering pipeline. Wrote investigation report to fleet investigations directory.
- Files: (no code changes, investigation only)
- Decisions: Primary bug is embedded newlines in displayBuffer entries breaking the renderer's row structure. Secondary bugs: editor accepting input during history mode, double render on AppState change, ANSI state leak in wrapLine, prepareEditor column mismatch, flushHistory without sync protocol.
- Next: Fix implementation per suggested order in report (Bug 1 embedded newlines first, Bug 2 history input guard, Bug 3 double render)

### 06:23 - release/publish (1.0.0-alpha.72)

- Did: Published GitHub release 1.0.0-alpha.72 (pre-release); npm-publish workflow succeeded; @shellicar/claude-cli@1.0.0-alpha.72 live on npm
- Files: (no code changes)
- Decisions: Auto-generated notes accepted; milestone 1.0 left open (pre-release series)
- Next: Nothing remaining for this release

### 06:12 - release/version-bump (1.0.0-alpha.72)

- Did: Bumped version to 1.0.0-alpha.72, updated CHANGELOG.md with 12 commits of rendering pipeline and scrollback changes, created PR shellicar/claude-cli#160 with auto-merge enabled
Expand Down
6 changes: 5 additions & 1 deletion src/ClaudeCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,11 @@ export class ClaudeCli {
return;
}

// Block editor input during history mode; navigation keys are handled above.
if (this.term.isHistoryMode) {
return;
}

switch (key.type) {
case 'ctrl+d': {
if (this.prompts.isOtherMode) {
Expand Down Expand Up @@ -1030,7 +1035,6 @@ export class ClaudeCli {
}, 300);
});
this.appState.on('changed', () => {
this.term.refresh();
this.redraw();
});

Expand Down
18 changes: 15 additions & 3 deletions src/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,11 @@ export class Terminal {
return this.historyViewport.mode === 'history';
}

/** Exposed for testing only. Do not use in production code. */
public get testDisplayBuffer(): readonly string[] {
return this.displayBuffer;
}

public scrollHistoryPageUp(): void {
this.historyViewport.pageUp();
}
Expand Down Expand Up @@ -398,14 +403,21 @@ export class Terminal {
}

private writeHistory(line: string): void {
const segments = line.split('\n');
if (this._paused) {
this.pauseBuffer.push(line);
for (const segment of segments) {
this.pauseBuffer.push(segment);
}
return;
}
this.displayBuffer.push(line);
for (const segment of segments) {
this.displayBuffer.push(segment);
}
if (this.inAltBuffer) {
// Accumulate in memory; re-render zone (status may have changed)
this.historyBuffer.push(line);
for (const segment of segments) {
this.historyBuffer.push(segment);
}
this.renderZone();
} else {
// Main buffer (startup messages, post-exit): write directly
Expand Down
56 changes: 56 additions & 0 deletions test/terminal.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { AppState } from '../src/AppState.js';
import { AttachmentStore } from '../src/AttachmentStore.js';
import { CommandMode } from '../src/CommandMode.js';
import { Terminal } from '../src/terminal.js';

function makeTerminal(): Terminal {
return new Terminal(new AppState(), null, new AttachmentStore(), new CommandMode());
}

describe('Terminal.writeHistory newline splitting', () => {
beforeEach(() => {
vi.spyOn(process.stdout, 'write').mockReturnValue(true);
});

afterEach(() => {
vi.restoreAllMocks();
});

it('single-line input produces one displayBuffer entry', () => {
const term = makeTerminal();
term.info('hello');
expect(term.testDisplayBuffer).toEqual(['hello']);
});

it('multi-line input produces separate displayBuffer entries per line', () => {
const term = makeTerminal();
term.info('line one\nline two');
expect(term.testDisplayBuffer).toEqual(['line one', 'line two']);
});

it('three-line input produces three displayBuffer entries', () => {
const term = makeTerminal();
term.info('a\nb\nc');
expect(term.testDisplayBuffer).toEqual(['a', 'b', 'c']);
});

it('consecutive newlines preserve empty entries', () => {
const term = makeTerminal();
term.info('before\n\nafter');
expect(term.testDisplayBuffer).toEqual(['before', '', 'after']);
});

it('trailing newline produces a trailing empty entry', () => {
const term = makeTerminal();
term.info('line\n');
expect(term.testDisplayBuffer).toEqual(['line', '']);
});

it('multiple info calls accumulate correctly', () => {
const term = makeTerminal();
term.info('first');
term.info('second\nthird');
expect(term.testDisplayBuffer).toEqual(['first', 'second', 'third']);
});
});
Loading