diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index eafebbf..66b432e 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -73,7 +73,7 @@ Only update the `Status` field — do not modify any other frontmatter or prompt ## Current State Branch: `main` -In-progress: PR #131 (fix #107, empty piped stdin hang) open, auto-merge enabled. +In-progress: Nothing. PR #136 (fix #133, sticky zone ANSI width math) open, auto-merge enabled. diff --git a/.claude/sessions/2026-03-27.md b/.claude/sessions/2026-03-27.md index 64694dd..bbebca3 100644 --- a/.claude/sessions/2026-03-27.md +++ b/.claude/sessions/2026-03-27.md @@ -43,3 +43,11 @@ - Decisions: None. - Next: PR #131 pending CI and auto-merge. - Violations: None + +### 18:25 - release/1.0.0-alpha.71 + +- Did: Added `verify-commit-signatures` workflow (copied from mcp-exec repo), consolidated duplicate `### Fixed` sections in CHANGELOG, added missing #122 fix entry, bumped version to 1.0.0-alpha.71, created PR #132 (auto-merge), published release after merge, confirmed 1.0.0-alpha.71 live on npm. +- Files: `.github/workflows/verify-commit-signatures.yml`, `CHANGELOG.md`, `package.json` +- Decisions: Used `feature/version-1.0.0-alpha.71` branch per shellicar-oss convention. Prompt status update to "in-progress" timed out (fleet repo outside working dir) but was resolved by the system. Release is a pre-release (contains hyphen) so `--prerelease` flag was used. +- Next: Nothing. Release complete. +- Violations: None diff --git a/.claude/sessions/2026-03-28.md b/.claude/sessions/2026-03-28.md new file mode 100644 index 0000000..0683745 --- /dev/null +++ b/.claude/sessions/2026-03-28.md @@ -0,0 +1,9 @@ +# Session Log: 2026-03-28 + +### 20:23 - fix/sticky-zone-width-math (#133) Stage 2 + +- Did: Staged and committed code changes from Stage 1 (src/ansi.ts, src/renderer.ts, src/terminal.ts plus leftover session-end files from March 27 release session), pushed branch, created PR #136 with Closes #133, auto-merge enabled. +- Files: `src/ansi.ts`, `src/renderer.ts`, `src/terminal.ts`, `.claude/CLAUDE.md`, `.claude/sessions/2026-03-27.md` +- Decisions: Bundled leftover staged session-end files from the March 27 release session into the code commit since they had not been committed. CI and CodeQL checks in progress; auto-merge will complete when checks pass. +- Next: Await PR #136 auto-merge. +- Violations: None diff --git a/src/ansi.ts b/src/ansi.ts new file mode 100644 index 0000000..1b54fe7 --- /dev/null +++ b/src/ansi.ts @@ -0,0 +1,11 @@ +// biome-ignore lint/suspicious/noControlCharactersInRegex: matching terminal escape sequences requires \x1b +const ANSI_PATTERN = /\x1b\[[0-9;]*m/g; + +/** + * Strips ANSI SGR escape codes and returns the visible character length. + * Handles colour, bold, inverse, and reset sequences only. + * Does not account for multi-width characters (emoji, CJK). + */ +export function stripAnsiLength(str: string): number { + return str.replace(ANSI_PATTERN, '').length; +} diff --git a/src/renderer.ts b/src/renderer.ts index d0899a8..f51ea63 100644 --- a/src/renderer.ts +++ b/src/renderer.ts @@ -3,6 +3,7 @@ * Computes what to display without writing to stdout. */ +import { stripAnsiLength } from './ansi.js'; import type { EditorState } from './editor.js'; const CONTINUATION = ' '; @@ -23,12 +24,13 @@ export function prepareEditor(editor: EditorState, prompt: string): EditorRender } const cursorPrefix = editor.cursor.row === 0 ? prompt : CONTINUATION; - const cursorCol = cursorPrefix.length + editor.cursor.col; + const cursorCol = stripAnsiLength(cursorPrefix) + editor.cursor.col; let cursorRow = 0; for (let i = 0; i < editor.cursor.row; i++) { - cursorRow += Math.max(1, Math.ceil(lines[i].length / columns)); + cursorRow += Math.max(1, Math.ceil(stripAnsiLength(lines[i]) / columns)); } + cursorRow += Math.floor(cursorCol / columns); return { lines, cursorRow, cursorCol }; } diff --git a/src/terminal.ts b/src/terminal.ts index 29cee06..128c035 100644 --- a/src/terminal.ts +++ b/src/terminal.ts @@ -2,6 +2,7 @@ import { inspect } from 'node:util'; import { DateTimeFormatter, LocalTime } from '@js-joda/core'; import type { AppState } from './AppState.js'; import type { AttachmentStore } from './AttachmentStore.js'; +import { stripAnsiLength } from './ansi.js'; import type { CommandMode } from './CommandMode.js'; import type { EditorState } from './editor.js'; import { type EditorRender, prepareEditor } from './renderer.js'; @@ -312,7 +313,7 @@ export class Terminal { for (let i = 0; i < this.editorContent.lines.length; i++) { output += '\n'; output += clearLine + this.editorContent.lines[i]; - editorScreenLines += Math.max(1, Math.ceil(this.editorContent.lines[i].length / columns)); + editorScreenLines += Math.max(1, Math.ceil(stripAnsiLength(this.editorContent.lines[i]) / columns)); } // Clear any leftover lines from previous render @@ -321,7 +322,7 @@ export class Terminal { // Position cursor within editor this.cursorLinesFromBottom = 0; for (let i = this.editorContent.lines.length - 1; i > this.editorContent.cursorRow; i--) { - this.cursorLinesFromBottom += Math.max(1, Math.ceil(this.editorContent.lines[i].length / columns)); + this.cursorLinesFromBottom += Math.max(1, Math.ceil(stripAnsiLength(this.editorContent.lines[i]) / columns)); } if (this.cursorLinesFromBottom > 0) { output += cursorUp(this.cursorLinesFromBottom);