diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 3871c92..53f0adc 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -76,8 +76,8 @@ Only update the `Status` field. Do not modify any other frontmatter or prompt co ## 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. diff --git a/.claude/sessions/2026-03-30.md b/.claude/sessions/2026-03-30.md index 4d090d1..52f26df 100644 --- a/.claude/sessions/2026-03-30.md +++ b/.claude/sessions/2026-03-30.md @@ -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 diff --git a/src/ClaudeCli.ts b/src/ClaudeCli.ts index 6aca687..21d11ea 100644 --- a/src/ClaudeCli.ts +++ b/src/ClaudeCli.ts @@ -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) { @@ -1030,7 +1035,6 @@ export class ClaudeCli { }, 300); }); this.appState.on('changed', () => { - this.term.refresh(); this.redraw(); }); diff --git a/src/terminal.ts b/src/terminal.ts index fb0cebe..8a4946c 100644 --- a/src/terminal.ts +++ b/src/terminal.ts @@ -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(); } @@ -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 diff --git a/test/terminal.spec.ts b/test/terminal.spec.ts new file mode 100644 index 0000000..0682815 --- /dev/null +++ b/test/terminal.spec.ts @@ -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']); + }); +});