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/153-zone-anchor-bottom`
In-progress: #153 Phase 1 complete. PR #156 open, auto-merge enabled.
Branch: `feature/154-155-scrollback-ux`
In-progress: PR shellicar/claude-cli#157 open, auto-merge enabled. Awaiting CI and merge.
<!-- END:REPO:current-state -->

<!-- BEGIN:REPO:architecture -->
Expand Down
14 changes: 14 additions & 0 deletions .claude/sessions/2026-03-30.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
### 05:47 - scrollback/ux Phase 2 (#154, #155)

- Did: Pushed branch, created PR shellicar/claude-cli#157 with auto-merge enabled
- Files: (no code changes)
- Decisions: Used milestone "1.0" (existing open milestone)
- Next: Awaiting CI and auto-merge

### 05:37 - scrollback/ux (#154, #155)

- Did: Implemented history mode zone collapse (editor hidden, zone = 1 indicator row); changed indicator from `[↑ start/total]` to `[↑ start-end/total]` with correct range; fixed resolve order so indicator reflects current scroll position on first render; added 6 tests
- Files: `src/terminal.ts`, `test/terminal-integration.spec.ts`
- Decisions: History mode resolves history viewport first (before building zone) to eliminate one-frame lag on indicator; `screenRows - 1` reserved for history gives full-screen effect with indicator still visible at bottom
- Next: Phase 2 (push branch + PR)

### 02:55: rendering/alt-buffer-design (Phase 0)

- Did: Investigated entire rendering pipeline for #149 alternate buffer rendering. Produced design artifacts for Phase 1/2 implementation.
Expand Down
24 changes: 22 additions & 2 deletions src/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,9 +192,10 @@ export class Terminal {

if (this.historyViewport.mode === 'history') {
const start = this.lastHistoryFrame.visibleStart + 1;
const end = Math.min(this.lastHistoryFrame.visibleStart + this.lastHistoryFrame.rows.length, this.lastHistoryFrame.totalLines);
const total = this.lastHistoryFrame.totalLines;
b.ansi(resetStyle);
b.text(` [\u2191 ${start}/${total}]`);
b.text(` [\u2191 ${start}-${end}/${total}]`);
}

return { line: b.output, screenLines: b.screenLines(columns) };
Expand Down Expand Up @@ -340,7 +341,27 @@ export class Terminal {
private renderZone(): void {
const columns = this.screen.columns;
const screenRows = this.screen.rows;
const wrappedHistory = this.displayBuffer.flatMap((line) => wrapLine(line, columns));

if (this.historyViewport.mode === 'history') {
// History mode: give history all but 1 row, zone collapses to just the indicator.
// Resolve history first so the indicator reflects the current scroll position.
const historyRowCount = Math.max(0, screenRows - 1);
const historyFrame = this.historyViewport.resolve(wrappedHistory, historyRowCount);
this.lastHistoryFrame = historyFrame;

const statusResult = this.buildStatusLine(columns, true);
const zoneBuffer = statusResult ? [statusResult.line] : [];
const zoneRows = screenRows - historyFrame.rows.length;
const frame = this.viewport.resolve(zoneBuffer, zoneRows, 0, 0);
this.renderer.render(historyFrame.rows, frame);
if (this.cursorHidden) {
this.screen.write(hideCursorSeq);
}
return;
}

// Live mode: full zone (editor + status + attachments).
// 1. Build zone
const input = this.buildLayoutInput(columns);
const result = layout(input);
Expand All @@ -350,7 +371,6 @@ export class Terminal {
const historyRows = screenRows - zoneHeight;

// 3. History viewport
const wrappedHistory = this.displayBuffer.flatMap((line) => wrapLine(line, columns));
const historyFrame = this.historyViewport.resolve(wrappedHistory, historyRows);
this.lastHistoryFrame = historyFrame;

Expand Down
85 changes: 85 additions & 0 deletions test/terminal-integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,91 @@ describe('Two-region rendering', () => {
expect(frame2.rows[2]).toBe('h');
});

it('history mode: history region gets screenRows-1 rows, zone gets 1', () => {
const screenRows = 10;
const historyViewport = new HistoryViewport();
const buffer = Array.from({ length: 28 }, (_, i) => `line ${i}`);

// Initialize in live mode so pageUp has lastViewportRows
historyViewport.resolve(buffer, screenRows - 1);
historyViewport.pageUp();

const historyFrame = historyViewport.resolve(buffer, screenRows - 1);
const zoneRows = screenRows - historyFrame.rows.length;

expect(historyFrame.rows.length).toBe(screenRows - 1);
expect(zoneRows).toBe(1);
});

it('history mode: returnToLive restores live mode', () => {
const historyViewport = new HistoryViewport();
const buffer = Array.from({ length: 28 }, (_, i) => `line ${i}`);

historyViewport.resolve(buffer, 9);
historyViewport.pageUp();
expect(historyViewport.mode).toBe('history');

historyViewport.returnToLive();
expect(historyViewport.mode).toBe('live');
});

it('indicator range: correct start-end for buffer larger than viewport', () => {
const historyViewport = new HistoryViewport();
const buffer = Array.from({ length: 52 }, (_, i) => `line ${i}`);
const historyRows = 24;

// Initialize and enter history mode
historyViewport.resolve(buffer, historyRows);
historyViewport.pageUp();
const frame = historyViewport.resolve(buffer, historyRows);

const start = frame.visibleStart + 1;
const end = Math.min(frame.visibleStart + frame.rows.length, frame.totalLines);

expect(start).toBeGreaterThanOrEqual(1);
expect(end).toBeGreaterThanOrEqual(start);
expect(end - start + 1).toBe(historyRows);
expect(end).toBeLessThanOrEqual(frame.totalLines);
});

it('indicator range: short buffer end is capped at totalLines not viewport rows', () => {
const historyViewport = new HistoryViewport();
const buffer = Array.from({ length: 10 }, (_, i) => `line ${i}`);
const historyRows = 24;

const frame = historyViewport.resolve(buffer, historyRows);

const start = frame.visibleStart + 1;
const end = Math.min(frame.visibleStart + frame.rows.length, frame.totalLines);

expect(start).toBe(1);
expect(end).toBe(10);
expect(frame.totalLines).toBe(10);
});

it('history mode: no scrollback violations with collapsed zone', () => {
const screenRows = 10;
const screen = new MockScreen(80, screenRows);
screen.enterAltBuffer();
const historyViewport = new HistoryViewport();
const viewport = new Viewport();
const renderer = new Renderer(screen);

const buffer = Array.from({ length: 28 }, (_, i) => `line ${i}`);

historyViewport.resolve(buffer, screenRows - 1);
historyViewport.pageUp();

const historyFrame = historyViewport.resolve(buffer, screenRows - 1);
const zoneRows = screenRows - historyFrame.rows.length;
const zoneBuffer = ['[\u2191 1-9/28]'];
const frame = viewport.resolve(zoneBuffer, zoneRows, 0, 0);

renderer.render(historyFrame.rows, frame);

screen.assertNoScrollbackViolations();
});

it('zone height changes do not corrupt history region', () => {
const screen = new MockScreen(80, 10);
screen.enterAltBuffer();
Expand Down
Loading