pi-startup-redraw-fix 0.1.12 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,22 @@
1
1
  # Changelog
2
2
 
3
+ ## [Unreleased]
4
+
5
+ ## [0.2.0] - 2026-07-03
6
+
7
+ ### Added
8
+ - Added an `enabled` master toggle (defaults to `true`) that gates extension startup, and structured debug logging written to an extension-local `debug/` directory when `debug` is enabled. ([2d57235](https://github.com/MasuRii/pi-startup-redraw-fix/commit/2d57235b6b72b6ec28f0a5298228d8fafe828da5))
9
+
10
+ ### Changed
11
+ - Widened Pi coding-agent and Pi TUI peer dependency ranges through `^0.80.0` and added a `postinstall` patch with npm `overrides` to resolve known vulnerabilities in transitive dependencies. ([2f1f400](https://github.com/MasuRii/pi-startup-redraw-fix/commit/2f1f400f804ee08b8f72f0f5d43ee6194de9b500))
12
+ - Extracted a fresh-terminal fixture helper for the terminal clear-sequence tests. ([0fc323d](https://github.com/MasuRii/pi-startup-redraw-fix/commit/0fc323d19a74ed9e7cdf070cb57015261e22865f))
13
+ - Updated README badge styling and added a ko-fi support button. ([0c3f0c3](https://github.com/MasuRii/pi-startup-redraw-fix/commit/0c3f0c36291a637dc47f2a5e8335eeb152e26244))
14
+
15
+ ## [0.1.13] - 2026-06-16
16
+
17
+ ### Fixed
18
+ - Buffered trailing broken clear-sequence prefixes across multiple `write` calls so split escape sequences that arrive in separate chunks are correctly normalized instead of being silently discarded.
19
+
3
20
  ## [0.1.12] - 2026-06-01
4
21
 
5
22
  ### Changed
package/README.md CHANGED
@@ -1,9 +1,17 @@
1
+ <div align="center">
2
+
1
3
  # pi-startup-redraw-fix
2
4
 
3
- A Pi coding agent extension that patches terminal full-clear escape sequence ordering to prevent startup redraw glitches in certain terminal emulators.
5
+ [![npm version](https://img.shields.io/npm/v/pi-startup-redraw-fix?style=for-the-badge)](https://www.npmjs.com/package/pi-startup-redraw-fix)
6
+ [![License](https://img.shields.io/github/license/MasuRii/pi-startup-redraw-fix?style=for-the-badge)](LICENSE)
7
+ [![Platform](https://img.shields.io/badge/Platform-macOS%20%7C%20Linux%20%7C%20Windows-blue?style=for-the-badge)]()
4
8
 
9
+ [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/Y8Y01PSSVR)
10
+
11
+ A Pi coding agent extension that patches terminal full-clear escape sequence ordering to prevent startup redraw glitches in certain terminal emulators.
5
12
  <img width="1360" height="752" alt="image" src="https://github.com/user-attachments/assets/05bfd443-052c-475e-bc80-3350cde5c642" />
6
13
 
14
+ </div>
7
15
 
8
16
  ## Table of Contents
9
17
 
@@ -1,3 +1,4 @@
1
- {
2
- "enabled": true
3
- }
1
+ {
2
+ "enabled": true,
3
+ "debug": false
4
+ }
package/package.json CHANGED
@@ -1,64 +1,72 @@
1
- {
2
- "name": "pi-startup-redraw-fix",
3
- "version": "0.1.12",
4
- "description": "Pi extension that patches terminal full-clear ordering to avoid startup redraw glitches.",
5
- "type": "module",
6
- "main": "./index.ts",
7
- "exports": {
8
- ".": "./index.ts"
9
- },
10
- "files": [
11
- "index.ts",
12
- "src",
13
- "asset",
14
- "config/config.example.json",
15
- "README.md",
16
- "CHANGELOG.md",
17
- "LICENSE"
18
- ],
19
- "scripts": {
20
- "build": "npx --yes -p typescript@5.7.3 tsc -p tsconfig.json --noCheck",
21
- "lint": "npm run build",
22
- "test": "npx --yes tsx --test normalize-clear-sequence.test.mjs",
23
- "check": "npm run lint && npm run test"
24
- },
25
- "keywords": [
26
- "pi-package",
27
- "pi",
28
- "pi-extension",
29
- "pi-coding-agent",
30
- "pi-tui",
31
- "coding-agent",
32
- "terminal",
33
- "terminal-emulator",
34
- "startup",
35
- "startup-fix",
36
- "redraw",
37
- "bugfix"
38
- ],
39
- "author": "MasuRii",
40
- "license": "MIT",
41
- "repository": {
42
- "type": "git",
43
- "url": "git+https://github.com/MasuRii/pi-startup-redraw-fix.git"
44
- },
45
- "bugs": {
46
- "url": "https://github.com/MasuRii/pi-startup-redraw-fix/issues"
47
- },
48
- "homepage": "https://github.com/MasuRii/pi-startup-redraw-fix#readme",
49
- "engines": {
50
- "node": ">=20"
51
- },
52
- "publishConfig": {
53
- "access": "public"
54
- },
55
- "pi": {
56
- "extensions": [
57
- "./index.ts"
58
- ]
59
- },
60
- "peerDependencies": {
61
- "@earendil-works/pi-coding-agent": "^0.74.0 || ^0.75.0 || ^0.77.0 || ^0.78.0",
62
- "@earendil-works/pi-tui": "^0.74.0 || ^0.75.0 || ^0.77.0 || ^0.78.0"
63
- }
64
- }
1
+ {
2
+ "name": "pi-startup-redraw-fix",
3
+ "version": "0.2.0",
4
+ "description": "Pi extension that patches terminal full-clear ordering to avoid startup redraw glitches.",
5
+ "type": "module",
6
+ "main": "./index.ts",
7
+ "exports": {
8
+ ".": "./index.ts"
9
+ },
10
+ "files": [
11
+ "index.ts",
12
+ "src",
13
+ "asset",
14
+ "config/config.example.json",
15
+ "README.md",
16
+ "CHANGELOG.md",
17
+ "LICENSE"
18
+ ],
19
+ "scripts": {
20
+ "build": "npx --yes -p typescript@5.7.3 tsc -p tsconfig.json --noCheck",
21
+ "lint": "npm run build",
22
+ "test": "npx --yes tsx --test normalize-clear-sequence.test.mjs",
23
+ "check": "npm run lint && npm run test",
24
+ "postinstall": "node -e \"const fs=require('fs'),cp=require('child_process'),p=require('path');const cwd=process.cwd();const normalized=cwd.split(p.sep).join('/');if(!normalized.includes('/.pi/agent/extensions/'))process.exit(0);const s=p.resolve(cwd,'../../scripts/patch-vulnerable-deps.mjs');if(!fs.existsSync(s))process.exit(0);const r=cp.spawnSync(process.execPath,[s,'--target',cwd,'--quiet'],{stdio:'inherit'});process.exit(r.status||0)\""
25
+ },
26
+ "keywords": [
27
+ "pi-package",
28
+ "pi",
29
+ "pi-extension",
30
+ "pi-coding-agent",
31
+ "pi-tui",
32
+ "coding-agent",
33
+ "terminal",
34
+ "terminal-emulator",
35
+ "startup",
36
+ "startup-fix",
37
+ "redraw",
38
+ "bugfix"
39
+ ],
40
+ "author": "MasuRii",
41
+ "license": "MIT",
42
+ "repository": {
43
+ "type": "git",
44
+ "url": "git+https://github.com/MasuRii/pi-startup-redraw-fix.git"
45
+ },
46
+ "bugs": {
47
+ "url": "https://github.com/MasuRii/pi-startup-redraw-fix/issues"
48
+ },
49
+ "homepage": "https://github.com/MasuRii/pi-startup-redraw-fix#readme",
50
+ "engines": {
51
+ "node": ">=20"
52
+ },
53
+ "publishConfig": {
54
+ "access": "public"
55
+ },
56
+ "pi": {
57
+ "extensions": [
58
+ "./index.ts"
59
+ ]
60
+ },
61
+ "peerDependencies": {
62
+ "@earendil-works/pi-coding-agent": "^0.74.0 || ^0.75.0 || ^0.77.0 || ^0.78.0 || ^0.79.0 || ^0.80.0",
63
+ "@earendil-works/pi-tui": "^0.74.0 || ^0.75.0 || ^0.77.0 || ^0.78.0 || ^0.79.0 || ^0.80.0"
64
+ },
65
+ "overrides": {
66
+ "protobufjs": "7.6.3",
67
+ "ws": "8.21.0"
68
+ },
69
+ "devDependencies": {
70
+ "@earendil-works/pi-coding-agent": "^0.80.3"
71
+ }
72
+ }
package/src/config.ts ADDED
@@ -0,0 +1,66 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+
5
+ export interface StartupRedrawFixConfig {
6
+ enabled: boolean;
7
+ debug: boolean;
8
+ }
9
+
10
+ const DEFAULT_CONFIG: StartupRedrawFixConfig = {
11
+ enabled: true,
12
+ debug: false,
13
+ };
14
+
15
+ function resolveExtensionRoot(moduleUrl = import.meta.url): string {
16
+ return join(dirname(fileURLToPath(moduleUrl)), "..");
17
+ }
18
+
19
+ export const EXTENSION_ROOT = resolveExtensionRoot();
20
+ export const CONFIG_PATH = join(EXTENSION_ROOT, "config.json");
21
+ export const DEBUG_DIR = join(EXTENSION_ROOT, "debug");
22
+ export const DEBUG_LOG_PATH = join(DEBUG_DIR, "debug.log");
23
+
24
+ type ReadConfigResult =
25
+ | { ok: true; record: Record<string, unknown> }
26
+ | { ok: false; reason: "missing" | "unparseable" | "not-object" };
27
+
28
+ /**
29
+ * Reads and validates the config file as a plain object.
30
+ *
31
+ * Returns a structured result so callers can react to each failure mode
32
+ * (missing, unparseable, non-object) without silently swallowing errors.
33
+ */
34
+ function readConfigRecord(configPath: string): ReadConfigResult {
35
+ if (!existsSync(configPath)) {
36
+ return { ok: false, reason: "missing" };
37
+ }
38
+
39
+ try {
40
+ const raw = readFileSync(configPath, "utf8");
41
+ const parsed = JSON.parse(raw) as unknown;
42
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
43
+ return { ok: false, reason: "not-object" };
44
+ }
45
+ return { ok: true, record: parsed as Record<string, unknown> };
46
+ } catch {
47
+ return { ok: false, reason: "unparseable" };
48
+ }
49
+ }
50
+
51
+ function readBoolean(record: Record<string, unknown>, key: keyof StartupRedrawFixConfig): boolean {
52
+ const value = record[key];
53
+ return typeof value === "boolean" ? value : DEFAULT_CONFIG[key];
54
+ }
55
+
56
+ export function loadStartupRedrawFixConfig(): StartupRedrawFixConfig {
57
+ const result = readConfigRecord(CONFIG_PATH);
58
+ if (!result.ok) {
59
+ return { ...DEFAULT_CONFIG };
60
+ }
61
+
62
+ return {
63
+ enabled: readBoolean(result.record, "enabled"),
64
+ debug: readBoolean(result.record, "debug"),
65
+ };
66
+ }
package/src/constants.ts CHANGED
@@ -1 +1 @@
1
- export { BROKEN_FULL_CLEAR_SEQUENCE, FIXED_FULL_CLEAR_SEQUENCE } from "./terminal-clear-patch.js";
1
+ export { BROKEN_FULL_CLEAR_SEQUENCE, FIXED_FULL_CLEAR_SEQUENCE } from "./terminal-clear-patch.js";
package/src/index.ts CHANGED
@@ -1,23 +1,37 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
-
3
- import { applyTerminalClearSequencePatch } from "./terminal-clear-patch.js";
4
-
5
- export default function startupRedrawFixExtension(pi: ExtensionAPI): void {
6
- // The terminal clear-sequence patch must be installed before the first
7
- // full-clear write at startup, so it is applied synchronously at registration
8
- // rather than deferred. The local import graph is tiny (a few dozen lines) and
9
- // the only package dependency, pi-tui, is already loaded by Pi core, so there
10
- // is no meaningful startup transpile cost to defer here.
11
- const patchResult = applyTerminalClearSequencePatch();
12
-
13
- pi.on("session_start", async (_event, ctx) => {
14
- if (!ctx.hasUI) {
15
- return;
16
- }
17
-
18
- if (!patchResult.patched && !patchResult.alreadyPatched) {
19
- const reason = patchResult.error ?? "unknown error";
20
- ctx.ui.notify(`startup-redraw-fix: failed to patch terminal clear sequence (${reason})`, "warning");
21
- }
22
- });
23
- }
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
+
3
+ import { loadStartupRedrawFixConfig } from "./config.js";
4
+ import { createStartupRedrawFixLogger } from "./logging.js";
5
+ import { applyTerminalClearSequencePatch } from "./terminal-clear-patch.js";
6
+
7
+ export default function startupRedrawFixExtension(pi: ExtensionAPI): void {
8
+ const config = loadStartupRedrawFixConfig();
9
+ if (!config.enabled) {
10
+ return;
11
+ }
12
+
13
+ const logger = createStartupRedrawFixLogger({ getConfig: () => config });
14
+
15
+ // The terminal clear-sequence patch must be installed before the first
16
+ // full-clear write at startup, so it is applied synchronously at registration
17
+ // rather than deferred. The local import graph is tiny (a few dozen lines) and
18
+ // the only package dependency, pi-tui, is already loaded by Pi core, so there
19
+ // is no meaningful startup transpile cost to defer here.
20
+ const patchResult = applyTerminalClearSequencePatch();
21
+ logger.debug("patch.applied", {
22
+ patched: patchResult.patched,
23
+ alreadyPatched: patchResult.alreadyPatched,
24
+ error: patchResult.error,
25
+ });
26
+
27
+ pi.on("session_start", async (_event, ctx) => {
28
+ if (!ctx.hasUI) {
29
+ return;
30
+ }
31
+
32
+ if (!patchResult.patched && !patchResult.alreadyPatched) {
33
+ const reason = patchResult.error ?? "unknown error";
34
+ ctx.ui.notify(`startup-redraw-fix: failed to patch terminal clear sequence (${reason})`, "warning");
35
+ }
36
+ });
37
+ }
package/src/logging.ts ADDED
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Debug logging for pi-startup-redraw-fix.
3
+ *
4
+ * Appends debug output to a file inside the colocated `debug/` directory
5
+ * when `debug` is true. Creates the directory at runtime only when debug
6
+ * logging is enabled. No console or stdio output.
7
+ */
8
+ import { mkdirSync } from "node:fs";
9
+ import { appendFile } from "node:fs/promises";
10
+
11
+ import { DEBUG_DIR, DEBUG_LOG_PATH } from "./config.js";
12
+
13
+ export interface StartupRedrawFixLogger {
14
+ /** Log a debug event with optional details. No-op when debug is false. */
15
+ debug: (event: string, details?: Record<string, unknown>) => void;
16
+ /** Wait for all queued debug writes to complete. */
17
+ flush: () => Promise<void>;
18
+ }
19
+
20
+ interface LoggerOptions {
21
+ /** Function returning the current config (checked on each call for live debug state). */
22
+ getConfig: () => { debug: boolean };
23
+ /** Override path for the debug log file. */
24
+ debugLogPath?: string;
25
+ /** Override path for the debug directory. */
26
+ debugDir?: string;
27
+ }
28
+
29
+ /**
30
+ * Creates a debug logger that writes to a colocated debug/ directory.
31
+ *
32
+ * - Debug output is only written when `config.debug` is true.
33
+ * - The debug/ directory is created at runtime only when debug logging is enabled.
34
+ * - All writes are async and queued to avoid blocking the extension.
35
+ * - No output is written to console, stdout, or stderr.
36
+ */
37
+ export function createStartupRedrawFixLogger(options: LoggerOptions): StartupRedrawFixLogger {
38
+ const logPath = options.debugLogPath ?? DEBUG_LOG_PATH;
39
+ const logDir = options.debugDir ?? DEBUG_DIR;
40
+ let writeQueue: Promise<void> = Promise.resolve();
41
+
42
+ const enqueueAppend = (line: string): void => {
43
+ writeQueue = writeQueue.then(
44
+ () => appendFile(logPath, `${line}\n`, "utf8"),
45
+ () => appendFile(logPath, `${line}\n`, "utf8"),
46
+ );
47
+ void writeQueue.catch(() => {
48
+ // Logging must never write to stdout/stderr or interrupt extension operation.
49
+ });
50
+ };
51
+
52
+ const debug = (event: string, details: Record<string, unknown> = {}): void => {
53
+ if (!options.getConfig().debug) {
54
+ return;
55
+ }
56
+
57
+ try {
58
+ mkdirSync(logDir, { recursive: true });
59
+ } catch {
60
+ // Cannot create debug directory — skip logging without disrupting startup.
61
+ return;
62
+ }
63
+
64
+ const line = JSON.stringify({
65
+ timestamp: new Date().toISOString(),
66
+ extension: "pi-startup-redraw-fix",
67
+ event,
68
+ ...details,
69
+ });
70
+ enqueueAppend(line);
71
+ };
72
+
73
+ const flush = (): Promise<void> => writeQueue.catch(() => undefined);
74
+
75
+ return { debug, flush };
76
+ }
@@ -1 +1 @@
1
- export { normalizeTerminalClearSequence } from "./terminal-clear-patch.js";
1
+ export { normalizeTerminalClearSequence } from "./terminal-clear-patch.js";
@@ -1,53 +1,83 @@
1
- import { ProcessTerminal } from "@earendil-works/pi-tui";
2
-
3
- export const BROKEN_FULL_CLEAR_SEQUENCE = "\x1b[3J\x1b[2J\x1b[H";
4
- export const FIXED_FULL_CLEAR_SEQUENCE = "\x1b[H\x1b[2J\x1b[3J";
5
-
6
- const PATCH_FLAG_KEY = "__piStartupRedrawFixPatched__" as const;
7
-
8
- type ProcessTerminalPrototype = typeof ProcessTerminal.prototype & {
9
- [PATCH_FLAG_KEY]?: boolean;
10
- };
11
-
12
- export interface PatchResult {
13
- patched: boolean;
14
- alreadyPatched: boolean;
15
- error?: string;
16
- }
17
-
18
- export function normalizeTerminalClearSequence(data: string): string {
19
- if (!data.includes(BROKEN_FULL_CLEAR_SEQUENCE)) {
20
- return data;
21
- }
22
-
23
- return data.split(BROKEN_FULL_CLEAR_SEQUENCE).join(FIXED_FULL_CLEAR_SEQUENCE);
24
- }
25
-
26
- export function applyTerminalClearSequencePatch(): PatchResult {
27
- const prototype = ProcessTerminal.prototype as ProcessTerminalPrototype;
28
-
29
- if (prototype[PATCH_FLAG_KEY]) {
30
- return { patched: false, alreadyPatched: true };
31
- }
32
-
33
- const originalWrite = prototype.write;
34
- if (typeof originalWrite !== "function") {
35
- return {
36
- patched: false,
37
- alreadyPatched: false,
38
- error: "ProcessTerminal.write is unavailable",
39
- };
40
- }
41
-
42
- prototype.write = function patchedProcessTerminalWrite(data: string): void {
43
- const normalized = typeof data === "string"
44
- ? normalizeTerminalClearSequence(data)
45
- : data;
46
-
47
- originalWrite.call(this, normalized);
48
- };
49
-
50
- prototype[PATCH_FLAG_KEY] = true;
51
-
52
- return { patched: true, alreadyPatched: false };
53
- }
1
+ import { ProcessTerminal } from "@earendil-works/pi-tui";
2
+
3
+ export const BROKEN_FULL_CLEAR_SEQUENCE = "\x1b[3J\x1b[2J\x1b[H";
4
+ export const FIXED_FULL_CLEAR_SEQUENCE = "\x1b[H\x1b[2J\x1b[3J";
5
+
6
+ const PATCH_FLAG_KEY = "__piStartupRedrawFixPatched__" as const;
7
+
8
+ type ProcessTerminalPrototype = typeof ProcessTerminal.prototype & {
9
+ [PATCH_FLAG_KEY]?: boolean;
10
+ };
11
+
12
+ export interface PatchResult {
13
+ patched: boolean;
14
+ alreadyPatched: boolean;
15
+ error?: string;
16
+ }
17
+
18
+ export function normalizeTerminalClearSequence(data: string): string {
19
+ if (!data.includes(BROKEN_FULL_CLEAR_SEQUENCE)) {
20
+ return data;
21
+ }
22
+
23
+ return data.split(BROKEN_FULL_CLEAR_SEQUENCE).join(FIXED_FULL_CLEAR_SEQUENCE);
24
+ }
25
+
26
+ function splitTrailingBrokenClearPrefix(data: string): { ready: string; pending: string } {
27
+ const maxPrefixLength = BROKEN_FULL_CLEAR_SEQUENCE.length - 1;
28
+ const maxCandidateLength = Math.min(maxPrefixLength, data.length);
29
+
30
+ for (let length = maxCandidateLength; length > 0; length -= 1) {
31
+ const suffix = data.slice(-length);
32
+ if (BROKEN_FULL_CLEAR_SEQUENCE.startsWith(suffix)) {
33
+ return {
34
+ ready: data.slice(0, -length),
35
+ pending: suffix,
36
+ };
37
+ }
38
+ }
39
+
40
+ return { ready: data, pending: "" };
41
+ }
42
+
43
+ export function applyTerminalClearSequencePatch(): PatchResult {
44
+ const prototype = ProcessTerminal.prototype as ProcessTerminalPrototype;
45
+
46
+ if (prototype[PATCH_FLAG_KEY]) {
47
+ return { patched: false, alreadyPatched: true };
48
+ }
49
+
50
+ const originalWrite = prototype.write;
51
+ if (typeof originalWrite !== "function") {
52
+ return {
53
+ patched: false,
54
+ alreadyPatched: false,
55
+ error: "ProcessTerminal.write is unavailable",
56
+ };
57
+ }
58
+
59
+ let pendingClearPrefix = "";
60
+
61
+ prototype.write = function patchedProcessTerminalWrite(data: string): void {
62
+ if (typeof data !== "string") {
63
+ if (pendingClearPrefix.length > 0) {
64
+ originalWrite.call(this, pendingClearPrefix);
65
+ pendingClearPrefix = "";
66
+ }
67
+ originalWrite.call(this, data);
68
+ return;
69
+ }
70
+
71
+ const combined = `${pendingClearPrefix}${data}`;
72
+ const { ready, pending } = splitTrailingBrokenClearPrefix(combined);
73
+ pendingClearPrefix = pending;
74
+
75
+ if (ready.length > 0) {
76
+ originalWrite.call(this, normalizeTerminalClearSequence(ready));
77
+ }
78
+ };
79
+
80
+ prototype[PATCH_FLAG_KEY] = true;
81
+
82
+ return { patched: true, alreadyPatched: false };
83
+ }