@mariozechner/pi-tui 0.37.7 → 0.37.8
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/dist/tui.d.ts +5 -0
- package/dist/tui.d.ts.map +1 -1
- package/dist/tui.js +5 -1
- package/dist/tui.js.map +1 -1
- package/package.json +1 -1
package/dist/tui.d.ts
CHANGED
|
@@ -17,6 +17,11 @@ export interface Component {
|
|
|
17
17
|
* Optional handler for keyboard input when component has focus
|
|
18
18
|
*/
|
|
19
19
|
handleInput?(data: string): void;
|
|
20
|
+
/**
|
|
21
|
+
* If true, component receives key release events (Kitty protocol).
|
|
22
|
+
* Default is false - release events are filtered out.
|
|
23
|
+
*/
|
|
24
|
+
wantsKeyRelease?: boolean;
|
|
20
25
|
/**
|
|
21
26
|
* Invalidate any cached rendering state.
|
|
22
27
|
* Called when theme changes or when component needs to re-render from scratch.
|
package/dist/tui.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tui.d.ts","sourceRoot":"","sources":["../src/tui.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1C;;GAEG;AACH,MAAM,WAAW,SAAS;IACzB;;;;OAIG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAEhC;;OAEG;IACH,WAAW,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAEjC;;;OAGG;IACH,UAAU,IAAI,IAAI,CAAC;CACnB;AAED,OAAO,EAAE,YAAY,EAAE,CAAC;AAExB;;GAEG;AACH,qBAAa,SAAU,YAAW,SAAS;IAC1C,QAAQ,EAAE,SAAS,EAAE,CAAM;IAE3B,QAAQ,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAEnC;IAED,WAAW,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAKtC;IAED,KAAK,IAAI,IAAI,CAEZ;IAED,UAAU,IAAI,IAAI,CAIjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAM9B;CACD;AAED;;GAEG;AACH,qBAAa,GAAI,SAAQ,SAAS;IAC1B,QAAQ,EAAE,QAAQ,CAAC;IAC1B,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,gBAAgB,CAA0B;IAElD,2GAA2G;IACpG,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IAC5B,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,WAAW,CAAM;IACzB,OAAO,CAAC,oBAAoB,CAAS;IAErC,YAAY,QAAQ,EAAE,QAAQ,EAG7B;IAED,QAAQ,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,GAAG,IAAI,CAE1C;IAED,KAAK,IAAI,IAAI,CAQZ;IAED,OAAO,CAAC,aAAa;IAWrB,IAAI,IAAI,IAAI,CAGX;IAED,aAAa,CAAC,KAAK,UAAQ,GAAG,IAAI,CAYjC;IAED,OAAO,CAAC,WAAW;IAuBnB,OAAO,CAAC,qBAAqB;IA0C7B,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,QAAQ;CA+IhB","sourcesContent":["/**\n * Minimal TUI implementation with differential rendering\n */\n\nimport * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { matchesKey } from \"./keys.js\";\nimport type { Terminal } from \"./terminal.js\";\nimport { getCapabilities, setCellDimensions } from \"./terminal-image.js\";\nimport { visibleWidth } from \"./utils.js\";\n\n/**\n * Component interface - all components must implement this\n */\nexport interface Component {\n\t/**\n\t * Render the component to lines for the given viewport width\n\t * @param width - Current viewport width\n\t * @returns Array of strings, each representing a line\n\t */\n\trender(width: number): string[];\n\n\t/**\n\t * Optional handler for keyboard input when component has focus\n\t */\n\thandleInput?(data: string): void;\n\n\t/**\n\t * Invalidate any cached rendering state.\n\t * Called when theme changes or when component needs to re-render from scratch.\n\t */\n\tinvalidate(): void;\n}\n\nexport { visibleWidth };\n\n/**\n * Container - a component that contains other components\n */\nexport class Container implements Component {\n\tchildren: Component[] = [];\n\n\taddChild(component: Component): void {\n\t\tthis.children.push(component);\n\t}\n\n\tremoveChild(component: Component): void {\n\t\tconst index = this.children.indexOf(component);\n\t\tif (index !== -1) {\n\t\t\tthis.children.splice(index, 1);\n\t\t}\n\t}\n\n\tclear(): void {\n\t\tthis.children = [];\n\t}\n\n\tinvalidate(): void {\n\t\tfor (const child of this.children) {\n\t\t\tchild.invalidate?.();\n\t\t}\n\t}\n\n\trender(width: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tfor (const child of this.children) {\n\t\t\tlines.push(...child.render(width));\n\t\t}\n\t\treturn lines;\n\t}\n}\n\n/**\n * TUI - Main class for managing terminal UI with differential rendering\n */\nexport class TUI extends Container {\n\tpublic terminal: Terminal;\n\tprivate previousLines: string[] = [];\n\tprivate previousWidth = 0;\n\tprivate focusedComponent: Component | null = null;\n\n\t/** Global callback for debug key (Shift+Ctrl+D). Called before input is forwarded to focused component. */\n\tpublic onDebug?: () => void;\n\tprivate renderRequested = false;\n\tprivate cursorRow = 0; // Track where cursor is (0-indexed, relative to our first line)\n\tprivate inputBuffer = \"\"; // Buffer for parsing terminal responses\n\tprivate cellSizeQueryPending = false;\n\n\tconstructor(terminal: Terminal) {\n\t\tsuper();\n\t\tthis.terminal = terminal;\n\t}\n\n\tsetFocus(component: Component | null): void {\n\t\tthis.focusedComponent = component;\n\t}\n\n\tstart(): void {\n\t\tthis.terminal.start(\n\t\t\t(data) => this.handleInput(data),\n\t\t\t() => this.requestRender(),\n\t\t);\n\t\tthis.terminal.hideCursor();\n\t\tthis.queryCellSize();\n\t\tthis.requestRender();\n\t}\n\n\tprivate queryCellSize(): void {\n\t\t// Only query if terminal supports images (cell size is only used for image rendering)\n\t\tif (!getCapabilities().images) {\n\t\t\treturn;\n\t\t}\n\t\t// Query terminal for cell size in pixels: CSI 16 t\n\t\t// Response format: CSI 6 ; height ; width t\n\t\tthis.cellSizeQueryPending = true;\n\t\tthis.terminal.write(\"\\x1b[16t\");\n\t}\n\n\tstop(): void {\n\t\tthis.terminal.showCursor();\n\t\tthis.terminal.stop();\n\t}\n\n\trequestRender(force = false): void {\n\t\tif (force) {\n\t\t\tthis.previousLines = [];\n\t\t\tthis.previousWidth = 0;\n\t\t\tthis.cursorRow = 0;\n\t\t}\n\t\tif (this.renderRequested) return;\n\t\tthis.renderRequested = true;\n\t\tprocess.nextTick(() => {\n\t\t\tthis.renderRequested = false;\n\t\t\tthis.doRender();\n\t\t});\n\t}\n\n\tprivate handleInput(data: string): void {\n\t\t// If we're waiting for cell size response, buffer input and parse\n\t\tif (this.cellSizeQueryPending) {\n\t\t\tthis.inputBuffer += data;\n\t\t\tconst filtered = this.parseCellSizeResponse();\n\t\t\tif (filtered.length === 0) return;\n\t\t\tdata = filtered;\n\t\t}\n\n\t\t// Global debug key handler (Shift+Ctrl+D)\n\t\tif (matchesKey(data, \"shift+ctrl+d\") && this.onDebug) {\n\t\t\tthis.onDebug();\n\t\t\treturn;\n\t\t}\n\n\t\t// Pass input to focused component (including Ctrl+C)\n\t\t// The focused component can decide how to handle Ctrl+C\n\t\tif (this.focusedComponent?.handleInput) {\n\t\t\tthis.focusedComponent.handleInput(data);\n\t\t\tthis.requestRender();\n\t\t}\n\t}\n\n\tprivate parseCellSizeResponse(): string {\n\t\t// Response format: ESC [ 6 ; height ; width t\n\t\t// Match the response pattern\n\t\tconst responsePattern = /\\x1b\\[6;(\\d+);(\\d+)t/;\n\t\tconst match = this.inputBuffer.match(responsePattern);\n\n\t\tif (match) {\n\t\t\tconst heightPx = parseInt(match[1], 10);\n\t\t\tconst widthPx = parseInt(match[2], 10);\n\n\t\t\tif (heightPx > 0 && widthPx > 0) {\n\t\t\t\tsetCellDimensions({ widthPx, heightPx });\n\t\t\t\t// Invalidate all components so images re-render with correct dimensions\n\t\t\t\tthis.invalidate();\n\t\t\t\tthis.requestRender();\n\t\t\t}\n\n\t\t\t// Remove the response from buffer\n\t\t\tthis.inputBuffer = this.inputBuffer.replace(responsePattern, \"\");\n\t\t\tthis.cellSizeQueryPending = false;\n\t\t}\n\n\t\t// Check if we have a partial cell size response starting (wait for more data)\n\t\t// Patterns that could be incomplete cell size response: \\x1b, \\x1b[, \\x1b[6, \\x1b[6;...(no t yet)\n\t\tconst partialCellSizePattern = /\\x1b(\\[6?;?[\\d;]*)?$/;\n\t\tif (partialCellSizePattern.test(this.inputBuffer)) {\n\t\t\t// Check if it's actually a complete different escape sequence (ends with a letter)\n\t\t\t// Cell size response ends with 't', Kitty keyboard ends with 'u', arrows end with A-D, etc.\n\t\t\tconst lastChar = this.inputBuffer[this.inputBuffer.length - 1];\n\t\t\tif (!/[a-zA-Z~]/.test(lastChar)) {\n\t\t\t\t// Doesn't end with a terminator, might be incomplete - wait for more\n\t\t\t\treturn \"\";\n\t\t\t}\n\t\t}\n\n\t\t// No cell size response found, return buffered data as user input\n\t\tconst result = this.inputBuffer;\n\t\tthis.inputBuffer = \"\";\n\t\tthis.cellSizeQueryPending = false; // Give up waiting\n\t\treturn result;\n\t}\n\n\tprivate containsImage(line: string): boolean {\n\t\treturn line.includes(\"\\x1b_G\") || line.includes(\"\\x1b]1337;File=\");\n\t}\n\n\tprivate doRender(): void {\n\t\tconst width = this.terminal.columns;\n\t\tconst height = this.terminal.rows;\n\n\t\t// Render all components to get new lines\n\t\tconst newLines = this.render(width);\n\n\t\t// Width changed - need full re-render\n\t\tconst widthChanged = this.previousWidth !== 0 && this.previousWidth !== width;\n\n\t\t// First render - just output everything without clearing\n\t\tif (this.previousLines.length === 0) {\n\t\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\t\t\tfor (let i = 0; i < newLines.length; i++) {\n\t\t\t\tif (i > 0) buffer += \"\\r\\n\";\n\t\t\t\tbuffer += newLines[i];\n\t\t\t}\n\t\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\t\t\tthis.terminal.write(buffer);\n\t\t\t// After rendering N lines, cursor is at end of last line (line N-1)\n\t\t\tthis.cursorRow = newLines.length - 1;\n\t\t\tthis.previousLines = newLines;\n\t\t\tthis.previousWidth = width;\n\t\t\treturn;\n\t\t}\n\n\t\t// Width changed - full re-render\n\t\tif (widthChanged) {\n\t\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\t\t\tbuffer += \"\\x1b[3J\\x1b[2J\\x1b[H\"; // Clear scrollback, screen, and home\n\t\t\tfor (let i = 0; i < newLines.length; i++) {\n\t\t\t\tif (i > 0) buffer += \"\\r\\n\";\n\t\t\t\tbuffer += newLines[i];\n\t\t\t}\n\t\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\t\t\tthis.terminal.write(buffer);\n\t\t\tthis.cursorRow = newLines.length - 1;\n\t\t\tthis.previousLines = newLines;\n\t\t\tthis.previousWidth = width;\n\t\t\treturn;\n\t\t}\n\n\t\t// Find first and last changed lines\n\t\tlet firstChanged = -1;\n\t\tconst maxLines = Math.max(newLines.length, this.previousLines.length);\n\t\tfor (let i = 0; i < maxLines; i++) {\n\t\t\tconst oldLine = i < this.previousLines.length ? this.previousLines[i] : \"\";\n\t\t\tconst newLine = i < newLines.length ? newLines[i] : \"\";\n\n\t\t\tif (oldLine !== newLine) {\n\t\t\t\tif (firstChanged === -1) {\n\t\t\t\t\tfirstChanged = i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// No changes\n\t\tif (firstChanged === -1) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Check if firstChanged is outside the viewport\n\t\t// cursorRow is the line where cursor is (0-indexed)\n\t\t// Viewport shows lines from (cursorRow - height + 1) to cursorRow\n\t\t// If firstChanged < viewportTop, we need full re-render\n\t\tconst viewportTop = this.cursorRow - height + 1;\n\t\tif (firstChanged < viewportTop) {\n\t\t\t// First change is above viewport - need full re-render\n\t\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\t\t\tbuffer += \"\\x1b[3J\\x1b[2J\\x1b[H\"; // Clear scrollback, screen, and home\n\t\t\tfor (let i = 0; i < newLines.length; i++) {\n\t\t\t\tif (i > 0) buffer += \"\\r\\n\";\n\t\t\t\tbuffer += newLines[i];\n\t\t\t}\n\t\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\t\t\tthis.terminal.write(buffer);\n\t\t\tthis.cursorRow = newLines.length - 1;\n\t\t\tthis.previousLines = newLines;\n\t\t\tthis.previousWidth = width;\n\t\t\treturn;\n\t\t}\n\n\t\t// Render from first changed line to end\n\t\t// Build buffer with all updates wrapped in synchronized output\n\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\n\t\t// Move cursor to first changed line\n\t\tconst lineDiff = firstChanged - this.cursorRow;\n\t\tif (lineDiff > 0) {\n\t\t\tbuffer += `\\x1b[${lineDiff}B`; // Move down\n\t\t} else if (lineDiff < 0) {\n\t\t\tbuffer += `\\x1b[${-lineDiff}A`; // Move up\n\t\t}\n\n\t\tbuffer += \"\\r\"; // Move to column 0\n\n\t\t// Render from first changed line to end, clearing each line before writing\n\t\t// This avoids the \\x1b[J clear-to-end which can cause flicker in xterm.js\n\t\tfor (let i = firstChanged; i < newLines.length; i++) {\n\t\t\tif (i > firstChanged) buffer += \"\\r\\n\";\n\t\t\tbuffer += \"\\x1b[2K\"; // Clear current line\n\t\t\tconst line = newLines[i];\n\t\t\tconst isImageLine = this.containsImage(line);\n\t\t\tif (!isImageLine && visibleWidth(line) > width) {\n\t\t\t\t// Log all lines to crash file for debugging\n\t\t\t\tconst crashLogPath = path.join(os.homedir(), \".pi\", \"agent\", \"pi-crash.log\");\n\t\t\t\tconst crashData = [\n\t\t\t\t\t`Crash at ${new Date().toISOString()}`,\n\t\t\t\t\t`Terminal width: ${width}`,\n\t\t\t\t\t`Line ${i} visible width: ${visibleWidth(line)}`,\n\t\t\t\t\t\"\",\n\t\t\t\t\t\"=== All rendered lines ===\",\n\t\t\t\t\t...newLines.map((l, idx) => `[${idx}] (w=${visibleWidth(l)}) ${l}`),\n\t\t\t\t\t\"\",\n\t\t\t\t].join(\"\\n\");\n\t\t\t\tfs.mkdirSync(path.dirname(crashLogPath), { recursive: true });\n\t\t\t\tfs.writeFileSync(crashLogPath, crashData);\n\t\t\t\tthrow new Error(`Rendered line ${i} exceeds terminal width. Debug log written to ${crashLogPath}`);\n\t\t\t}\n\t\t\tbuffer += line;\n\t\t}\n\n\t\t// If we had more lines before, clear them and move cursor back\n\t\tif (this.previousLines.length > newLines.length) {\n\t\t\tconst extraLines = this.previousLines.length - newLines.length;\n\t\t\tfor (let i = newLines.length; i < this.previousLines.length; i++) {\n\t\t\t\tbuffer += \"\\r\\n\\x1b[2K\";\n\t\t\t}\n\t\t\t// Move cursor back to end of new content\n\t\t\tbuffer += `\\x1b[${extraLines}A`;\n\t\t}\n\n\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\n\t\t// Write entire buffer at once\n\t\tthis.terminal.write(buffer);\n\n\t\t// Cursor is now at end of last line\n\t\tthis.cursorRow = newLines.length - 1;\n\n\t\tthis.previousLines = newLines;\n\t\tthis.previousWidth = width;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"tui.d.ts","sourceRoot":"","sources":["../src/tui.ts"],"names":[],"mappings":"AAAA;;GAEG;AAMH,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AAE9C,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAE1C;;GAEG;AACH,MAAM,WAAW,SAAS;IACzB;;;;OAIG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAAC;IAEhC;;OAEG;IACH,WAAW,CAAC,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAC;IAEjC;;;OAGG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAE1B;;;OAGG;IACH,UAAU,IAAI,IAAI,CAAC;CACnB;AAED,OAAO,EAAE,YAAY,EAAE,CAAC;AAExB;;GAEG;AACH,qBAAa,SAAU,YAAW,SAAS;IAC1C,QAAQ,EAAE,SAAS,EAAE,CAAM;IAE3B,QAAQ,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAEnC;IAED,WAAW,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,CAKtC;IAED,KAAK,IAAI,IAAI,CAEZ;IAED,UAAU,IAAI,IAAI,CAIjB;IAED,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAM9B;CACD;AAED;;GAEG;AACH,qBAAa,GAAI,SAAQ,SAAS;IAC1B,QAAQ,EAAE,QAAQ,CAAC;IAC1B,OAAO,CAAC,aAAa,CAAgB;IACrC,OAAO,CAAC,aAAa,CAAK;IAC1B,OAAO,CAAC,gBAAgB,CAA0B;IAElD,2GAA2G;IACpG,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IAC5B,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,SAAS,CAAK;IACtB,OAAO,CAAC,WAAW,CAAM;IACzB,OAAO,CAAC,oBAAoB,CAAS;IAErC,YAAY,QAAQ,EAAE,QAAQ,EAG7B;IAED,QAAQ,CAAC,SAAS,EAAE,SAAS,GAAG,IAAI,GAAG,IAAI,CAE1C;IAED,KAAK,IAAI,IAAI,CAQZ;IAED,OAAO,CAAC,aAAa;IAWrB,IAAI,IAAI,IAAI,CAGX;IAED,aAAa,CAAC,KAAK,UAAQ,GAAG,IAAI,CAYjC;IAED,OAAO,CAAC,WAAW;IA2BnB,OAAO,CAAC,qBAAqB;IA0C7B,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,QAAQ;CA+IhB","sourcesContent":["/**\n * Minimal TUI implementation with differential rendering\n */\n\nimport * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { isKeyRelease, matchesKey } from \"./keys.js\";\nimport type { Terminal } from \"./terminal.js\";\nimport { getCapabilities, setCellDimensions } from \"./terminal-image.js\";\nimport { visibleWidth } from \"./utils.js\";\n\n/**\n * Component interface - all components must implement this\n */\nexport interface Component {\n\t/**\n\t * Render the component to lines for the given viewport width\n\t * @param width - Current viewport width\n\t * @returns Array of strings, each representing a line\n\t */\n\trender(width: number): string[];\n\n\t/**\n\t * Optional handler for keyboard input when component has focus\n\t */\n\thandleInput?(data: string): void;\n\n\t/**\n\t * If true, component receives key release events (Kitty protocol).\n\t * Default is false - release events are filtered out.\n\t */\n\twantsKeyRelease?: boolean;\n\n\t/**\n\t * Invalidate any cached rendering state.\n\t * Called when theme changes or when component needs to re-render from scratch.\n\t */\n\tinvalidate(): void;\n}\n\nexport { visibleWidth };\n\n/**\n * Container - a component that contains other components\n */\nexport class Container implements Component {\n\tchildren: Component[] = [];\n\n\taddChild(component: Component): void {\n\t\tthis.children.push(component);\n\t}\n\n\tremoveChild(component: Component): void {\n\t\tconst index = this.children.indexOf(component);\n\t\tif (index !== -1) {\n\t\t\tthis.children.splice(index, 1);\n\t\t}\n\t}\n\n\tclear(): void {\n\t\tthis.children = [];\n\t}\n\n\tinvalidate(): void {\n\t\tfor (const child of this.children) {\n\t\t\tchild.invalidate?.();\n\t\t}\n\t}\n\n\trender(width: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tfor (const child of this.children) {\n\t\t\tlines.push(...child.render(width));\n\t\t}\n\t\treturn lines;\n\t}\n}\n\n/**\n * TUI - Main class for managing terminal UI with differential rendering\n */\nexport class TUI extends Container {\n\tpublic terminal: Terminal;\n\tprivate previousLines: string[] = [];\n\tprivate previousWidth = 0;\n\tprivate focusedComponent: Component | null = null;\n\n\t/** Global callback for debug key (Shift+Ctrl+D). Called before input is forwarded to focused component. */\n\tpublic onDebug?: () => void;\n\tprivate renderRequested = false;\n\tprivate cursorRow = 0; // Track where cursor is (0-indexed, relative to our first line)\n\tprivate inputBuffer = \"\"; // Buffer for parsing terminal responses\n\tprivate cellSizeQueryPending = false;\n\n\tconstructor(terminal: Terminal) {\n\t\tsuper();\n\t\tthis.terminal = terminal;\n\t}\n\n\tsetFocus(component: Component | null): void {\n\t\tthis.focusedComponent = component;\n\t}\n\n\tstart(): void {\n\t\tthis.terminal.start(\n\t\t\t(data) => this.handleInput(data),\n\t\t\t() => this.requestRender(),\n\t\t);\n\t\tthis.terminal.hideCursor();\n\t\tthis.queryCellSize();\n\t\tthis.requestRender();\n\t}\n\n\tprivate queryCellSize(): void {\n\t\t// Only query if terminal supports images (cell size is only used for image rendering)\n\t\tif (!getCapabilities().images) {\n\t\t\treturn;\n\t\t}\n\t\t// Query terminal for cell size in pixels: CSI 16 t\n\t\t// Response format: CSI 6 ; height ; width t\n\t\tthis.cellSizeQueryPending = true;\n\t\tthis.terminal.write(\"\\x1b[16t\");\n\t}\n\n\tstop(): void {\n\t\tthis.terminal.showCursor();\n\t\tthis.terminal.stop();\n\t}\n\n\trequestRender(force = false): void {\n\t\tif (force) {\n\t\t\tthis.previousLines = [];\n\t\t\tthis.previousWidth = 0;\n\t\t\tthis.cursorRow = 0;\n\t\t}\n\t\tif (this.renderRequested) return;\n\t\tthis.renderRequested = true;\n\t\tprocess.nextTick(() => {\n\t\t\tthis.renderRequested = false;\n\t\t\tthis.doRender();\n\t\t});\n\t}\n\n\tprivate handleInput(data: string): void {\n\t\t// If we're waiting for cell size response, buffer input and parse\n\t\tif (this.cellSizeQueryPending) {\n\t\t\tthis.inputBuffer += data;\n\t\t\tconst filtered = this.parseCellSizeResponse();\n\t\t\tif (filtered.length === 0) return;\n\t\t\tdata = filtered;\n\t\t}\n\n\t\t// Global debug key handler (Shift+Ctrl+D)\n\t\tif (matchesKey(data, \"shift+ctrl+d\") && this.onDebug) {\n\t\t\tthis.onDebug();\n\t\t\treturn;\n\t\t}\n\n\t\t// Pass input to focused component (including Ctrl+C)\n\t\t// The focused component can decide how to handle Ctrl+C\n\t\tif (this.focusedComponent?.handleInput) {\n\t\t\t// Filter out key release events unless component opts in\n\t\t\tif (isKeyRelease(data) && !this.focusedComponent.wantsKeyRelease) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.focusedComponent.handleInput(data);\n\t\t\tthis.requestRender();\n\t\t}\n\t}\n\n\tprivate parseCellSizeResponse(): string {\n\t\t// Response format: ESC [ 6 ; height ; width t\n\t\t// Match the response pattern\n\t\tconst responsePattern = /\\x1b\\[6;(\\d+);(\\d+)t/;\n\t\tconst match = this.inputBuffer.match(responsePattern);\n\n\t\tif (match) {\n\t\t\tconst heightPx = parseInt(match[1], 10);\n\t\t\tconst widthPx = parseInt(match[2], 10);\n\n\t\t\tif (heightPx > 0 && widthPx > 0) {\n\t\t\t\tsetCellDimensions({ widthPx, heightPx });\n\t\t\t\t// Invalidate all components so images re-render with correct dimensions\n\t\t\t\tthis.invalidate();\n\t\t\t\tthis.requestRender();\n\t\t\t}\n\n\t\t\t// Remove the response from buffer\n\t\t\tthis.inputBuffer = this.inputBuffer.replace(responsePattern, \"\");\n\t\t\tthis.cellSizeQueryPending = false;\n\t\t}\n\n\t\t// Check if we have a partial cell size response starting (wait for more data)\n\t\t// Patterns that could be incomplete cell size response: \\x1b, \\x1b[, \\x1b[6, \\x1b[6;...(no t yet)\n\t\tconst partialCellSizePattern = /\\x1b(\\[6?;?[\\d;]*)?$/;\n\t\tif (partialCellSizePattern.test(this.inputBuffer)) {\n\t\t\t// Check if it's actually a complete different escape sequence (ends with a letter)\n\t\t\t// Cell size response ends with 't', Kitty keyboard ends with 'u', arrows end with A-D, etc.\n\t\t\tconst lastChar = this.inputBuffer[this.inputBuffer.length - 1];\n\t\t\tif (!/[a-zA-Z~]/.test(lastChar)) {\n\t\t\t\t// Doesn't end with a terminator, might be incomplete - wait for more\n\t\t\t\treturn \"\";\n\t\t\t}\n\t\t}\n\n\t\t// No cell size response found, return buffered data as user input\n\t\tconst result = this.inputBuffer;\n\t\tthis.inputBuffer = \"\";\n\t\tthis.cellSizeQueryPending = false; // Give up waiting\n\t\treturn result;\n\t}\n\n\tprivate containsImage(line: string): boolean {\n\t\treturn line.includes(\"\\x1b_G\") || line.includes(\"\\x1b]1337;File=\");\n\t}\n\n\tprivate doRender(): void {\n\t\tconst width = this.terminal.columns;\n\t\tconst height = this.terminal.rows;\n\n\t\t// Render all components to get new lines\n\t\tconst newLines = this.render(width);\n\n\t\t// Width changed - need full re-render\n\t\tconst widthChanged = this.previousWidth !== 0 && this.previousWidth !== width;\n\n\t\t// First render - just output everything without clearing\n\t\tif (this.previousLines.length === 0) {\n\t\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\t\t\tfor (let i = 0; i < newLines.length; i++) {\n\t\t\t\tif (i > 0) buffer += \"\\r\\n\";\n\t\t\t\tbuffer += newLines[i];\n\t\t\t}\n\t\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\t\t\tthis.terminal.write(buffer);\n\t\t\t// After rendering N lines, cursor is at end of last line (line N-1)\n\t\t\tthis.cursorRow = newLines.length - 1;\n\t\t\tthis.previousLines = newLines;\n\t\t\tthis.previousWidth = width;\n\t\t\treturn;\n\t\t}\n\n\t\t// Width changed - full re-render\n\t\tif (widthChanged) {\n\t\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\t\t\tbuffer += \"\\x1b[3J\\x1b[2J\\x1b[H\"; // Clear scrollback, screen, and home\n\t\t\tfor (let i = 0; i < newLines.length; i++) {\n\t\t\t\tif (i > 0) buffer += \"\\r\\n\";\n\t\t\t\tbuffer += newLines[i];\n\t\t\t}\n\t\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\t\t\tthis.terminal.write(buffer);\n\t\t\tthis.cursorRow = newLines.length - 1;\n\t\t\tthis.previousLines = newLines;\n\t\t\tthis.previousWidth = width;\n\t\t\treturn;\n\t\t}\n\n\t\t// Find first and last changed lines\n\t\tlet firstChanged = -1;\n\t\tconst maxLines = Math.max(newLines.length, this.previousLines.length);\n\t\tfor (let i = 0; i < maxLines; i++) {\n\t\t\tconst oldLine = i < this.previousLines.length ? this.previousLines[i] : \"\";\n\t\t\tconst newLine = i < newLines.length ? newLines[i] : \"\";\n\n\t\t\tif (oldLine !== newLine) {\n\t\t\t\tif (firstChanged === -1) {\n\t\t\t\t\tfirstChanged = i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// No changes\n\t\tif (firstChanged === -1) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Check if firstChanged is outside the viewport\n\t\t// cursorRow is the line where cursor is (0-indexed)\n\t\t// Viewport shows lines from (cursorRow - height + 1) to cursorRow\n\t\t// If firstChanged < viewportTop, we need full re-render\n\t\tconst viewportTop = this.cursorRow - height + 1;\n\t\tif (firstChanged < viewportTop) {\n\t\t\t// First change is above viewport - need full re-render\n\t\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\t\t\tbuffer += \"\\x1b[3J\\x1b[2J\\x1b[H\"; // Clear scrollback, screen, and home\n\t\t\tfor (let i = 0; i < newLines.length; i++) {\n\t\t\t\tif (i > 0) buffer += \"\\r\\n\";\n\t\t\t\tbuffer += newLines[i];\n\t\t\t}\n\t\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\t\t\tthis.terminal.write(buffer);\n\t\t\tthis.cursorRow = newLines.length - 1;\n\t\t\tthis.previousLines = newLines;\n\t\t\tthis.previousWidth = width;\n\t\t\treturn;\n\t\t}\n\n\t\t// Render from first changed line to end\n\t\t// Build buffer with all updates wrapped in synchronized output\n\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\n\t\t// Move cursor to first changed line\n\t\tconst lineDiff = firstChanged - this.cursorRow;\n\t\tif (lineDiff > 0) {\n\t\t\tbuffer += `\\x1b[${lineDiff}B`; // Move down\n\t\t} else if (lineDiff < 0) {\n\t\t\tbuffer += `\\x1b[${-lineDiff}A`; // Move up\n\t\t}\n\n\t\tbuffer += \"\\r\"; // Move to column 0\n\n\t\t// Render from first changed line to end, clearing each line before writing\n\t\t// This avoids the \\x1b[J clear-to-end which can cause flicker in xterm.js\n\t\tfor (let i = firstChanged; i < newLines.length; i++) {\n\t\t\tif (i > firstChanged) buffer += \"\\r\\n\";\n\t\t\tbuffer += \"\\x1b[2K\"; // Clear current line\n\t\t\tconst line = newLines[i];\n\t\t\tconst isImageLine = this.containsImage(line);\n\t\t\tif (!isImageLine && visibleWidth(line) > width) {\n\t\t\t\t// Log all lines to crash file for debugging\n\t\t\t\tconst crashLogPath = path.join(os.homedir(), \".pi\", \"agent\", \"pi-crash.log\");\n\t\t\t\tconst crashData = [\n\t\t\t\t\t`Crash at ${new Date().toISOString()}`,\n\t\t\t\t\t`Terminal width: ${width}`,\n\t\t\t\t\t`Line ${i} visible width: ${visibleWidth(line)}`,\n\t\t\t\t\t\"\",\n\t\t\t\t\t\"=== All rendered lines ===\",\n\t\t\t\t\t...newLines.map((l, idx) => `[${idx}] (w=${visibleWidth(l)}) ${l}`),\n\t\t\t\t\t\"\",\n\t\t\t\t].join(\"\\n\");\n\t\t\t\tfs.mkdirSync(path.dirname(crashLogPath), { recursive: true });\n\t\t\t\tfs.writeFileSync(crashLogPath, crashData);\n\t\t\t\tthrow new Error(`Rendered line ${i} exceeds terminal width. Debug log written to ${crashLogPath}`);\n\t\t\t}\n\t\t\tbuffer += line;\n\t\t}\n\n\t\t// If we had more lines before, clear them and move cursor back\n\t\tif (this.previousLines.length > newLines.length) {\n\t\t\tconst extraLines = this.previousLines.length - newLines.length;\n\t\t\tfor (let i = newLines.length; i < this.previousLines.length; i++) {\n\t\t\t\tbuffer += \"\\r\\n\\x1b[2K\";\n\t\t\t}\n\t\t\t// Move cursor back to end of new content\n\t\t\tbuffer += `\\x1b[${extraLines}A`;\n\t\t}\n\n\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\n\t\t// Write entire buffer at once\n\t\tthis.terminal.write(buffer);\n\n\t\t// Cursor is now at end of last line\n\t\tthis.cursorRow = newLines.length - 1;\n\n\t\tthis.previousLines = newLines;\n\t\tthis.previousWidth = width;\n\t}\n}\n"]}
|
package/dist/tui.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
import * as fs from "node:fs";
|
|
5
5
|
import * as os from "node:os";
|
|
6
6
|
import * as path from "node:path";
|
|
7
|
-
import { matchesKey } from "./keys.js";
|
|
7
|
+
import { isKeyRelease, matchesKey } from "./keys.js";
|
|
8
8
|
import { getCapabilities, setCellDimensions } from "./terminal-image.js";
|
|
9
9
|
import { visibleWidth } from "./utils.js";
|
|
10
10
|
export { visibleWidth };
|
|
@@ -110,6 +110,10 @@ export class TUI extends Container {
|
|
|
110
110
|
// Pass input to focused component (including Ctrl+C)
|
|
111
111
|
// The focused component can decide how to handle Ctrl+C
|
|
112
112
|
if (this.focusedComponent?.handleInput) {
|
|
113
|
+
// Filter out key release events unless component opts in
|
|
114
|
+
if (isKeyRelease(data) && !this.focusedComponent.wantsKeyRelease) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
113
117
|
this.focusedComponent.handleInput(data);
|
|
114
118
|
this.requestRender();
|
|
115
119
|
}
|
package/dist/tui.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tui.js","sourceRoot":"","sources":["../src/tui.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAEvC,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACzE,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAyB1C,OAAO,EAAE,YAAY,EAAE,CAAC;AAExB;;GAEG;AACH,MAAM,OAAO,SAAS;IACrB,QAAQ,GAAgB,EAAE,CAAC;IAE3B,QAAQ,CAAC,SAAoB,EAAQ;QACpC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAAA,CAC9B;IAED,WAAW,CAAC,SAAoB,EAAQ;QACvC,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAC/C,IAAI,KAAK,KAAK,CAAC,CAAC,EAAE,CAAC;YAClB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;QAChC,CAAC;IAAA,CACD;IAED,KAAK,GAAS;QACb,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAC;IAAA,CACnB;IAED,UAAU,GAAS;QAClB,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnC,KAAK,CAAC,UAAU,EAAE,EAAE,CAAC;QACtB,CAAC;IAAA,CACD;IAED,MAAM,CAAC,KAAa,EAAY;QAC/B,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnC,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACpC,CAAC;QACD,OAAO,KAAK,CAAC;IAAA,CACb;CACD;AAED;;GAEG;AACH,MAAM,OAAO,GAAI,SAAQ,SAAS;IAC1B,QAAQ,CAAW;IAClB,aAAa,GAAa,EAAE,CAAC;IAC7B,aAAa,GAAG,CAAC,CAAC;IAClB,gBAAgB,GAAqB,IAAI,CAAC;IAElD,2GAA2G;IACpG,OAAO,CAAc;IACpB,eAAe,GAAG,KAAK,CAAC;IACxB,SAAS,GAAG,CAAC,CAAC,CAAC,gEAAgE;IAC/E,WAAW,GAAG,EAAE,CAAC,CAAC,wCAAwC;IAC1D,oBAAoB,GAAG,KAAK,CAAC;IAErC,YAAY,QAAkB,EAAE;QAC/B,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAAA,CACzB;IAED,QAAQ,CAAC,SAA2B,EAAQ;QAC3C,IAAI,CAAC,gBAAgB,GAAG,SAAS,CAAC;IAAA,CAClC;IAED,KAAK,GAAS;QACb,IAAI,CAAC,QAAQ,CAAC,KAAK,CAClB,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,EAChC,GAAG,EAAE,CAAC,IAAI,CAAC,aAAa,EAAE,CAC1B,CAAC;QACF,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,CAAC;QAC3B,IAAI,CAAC,aAAa,EAAE,CAAC;QACrB,IAAI,CAAC,aAAa,EAAE,CAAC;IAAA,CACrB;IAEO,aAAa,GAAS;QAC7B,sFAAsF;QACtF,IAAI,CAAC,eAAe,EAAE,CAAC,MAAM,EAAE,CAAC;YAC/B,OAAO;QACR,CAAC;QACD,mDAAmD;QACnD,4CAA4C;QAC5C,IAAI,CAAC,oBAAoB,GAAG,IAAI,CAAC;QACjC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;IAAA,CAChC;IAED,IAAI,GAAS;QACZ,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,CAAC;QAC3B,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;IAAA,CACrB;IAED,aAAa,CAAC,KAAK,GAAG,KAAK,EAAQ;QAClC,IAAI,KAAK,EAAE,CAAC;YACX,IAAI,CAAC,aAAa,GAAG,EAAE,CAAC;YACxB,IAAI,CAAC,aAAa,GAAG,CAAC,CAAC;YACvB,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC;QACpB,CAAC;QACD,IAAI,IAAI,CAAC,eAAe;YAAE,OAAO;QACjC,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;QAC5B,OAAO,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC;YACtB,IAAI,CAAC,eAAe,GAAG,KAAK,CAAC;YAC7B,IAAI,CAAC,QAAQ,EAAE,CAAC;QAAA,CAChB,CAAC,CAAC;IAAA,CACH;IAEO,WAAW,CAAC,IAAY,EAAQ;QACvC,kEAAkE;QAClE,IAAI,IAAI,CAAC,oBAAoB,EAAE,CAAC;YAC/B,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC;YACzB,MAAM,QAAQ,GAAG,IAAI,CAAC,qBAAqB,EAAE,CAAC;YAC9C,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO;YAClC,IAAI,GAAG,QAAQ,CAAC;QACjB,CAAC;QAED,0CAA0C;QAC1C,IAAI,UAAU,CAAC,IAAI,EAAE,cAAc,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACtD,IAAI,CAAC,OAAO,EAAE,CAAC;YACf,OAAO;QACR,CAAC;QAED,qDAAqD;QACrD,wDAAwD;QACxD,IAAI,IAAI,CAAC,gBAAgB,EAAE,WAAW,EAAE,CAAC;YACxC,IAAI,CAAC,gBAAgB,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;YACxC,IAAI,CAAC,aAAa,EAAE,CAAC;QACtB,CAAC;IAAA,CACD;IAEO,qBAAqB,GAAW;QACvC,8CAA8C;QAC9C,6BAA6B;QAC7B,MAAM,eAAe,GAAG,sBAAsB,CAAC;QAC/C,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;QAEtD,IAAI,KAAK,EAAE,CAAC;YACX,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACxC,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAEvC,IAAI,QAAQ,GAAG,CAAC,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;gBACjC,iBAAiB,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC;gBACzC,wEAAwE;gBACxE,IAAI,CAAC,UAAU,EAAE,CAAC;gBAClB,IAAI,CAAC,aAAa,EAAE,CAAC;YACtB,CAAC;YAED,kCAAkC;YAClC,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC;YACjE,IAAI,CAAC,oBAAoB,GAAG,KAAK,CAAC;QACnC,CAAC;QAED,8EAA8E;QAC9E,kGAAkG;QAClG,MAAM,sBAAsB,GAAG,sBAAsB,CAAC;QACtD,IAAI,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;YACnD,mFAAmF;YACnF,4FAA4F;YAC5F,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YAC/D,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACjC,qEAAqE;gBACrE,OAAO,EAAE,CAAC;YACX,CAAC;QACF,CAAC;QAED,kEAAkE;QAClE,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,CAAC;QAChC,IAAI,CAAC,WAAW,GAAG,EAAE,CAAC;QACtB,IAAI,CAAC,oBAAoB,GAAG,KAAK,CAAC,CAAC,kBAAkB;QACrD,OAAO,MAAM,CAAC;IAAA,CACd;IAEO,aAAa,CAAC,IAAY,EAAW;QAC5C,OAAO,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,iBAAiB,CAAC,CAAC;IAAA,CACnE;IAEO,QAAQ,GAAS;QACxB,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;QACpC,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;QAElC,yCAAyC;QACzC,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAEpC,sCAAsC;QACtC,MAAM,YAAY,GAAG,IAAI,CAAC,aAAa,KAAK,CAAC,IAAI,IAAI,CAAC,aAAa,KAAK,KAAK,CAAC;QAE9E,yDAAyD;QACzD,IAAI,IAAI,CAAC,aAAa,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACrC,IAAI,MAAM,GAAG,aAAa,CAAC,CAAC,4BAA4B;YACxD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;gBAC1C,IAAI,CAAC,GAAG,CAAC;oBAAE,MAAM,IAAI,MAAM,CAAC;gBAC5B,MAAM,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC;YACvB,CAAC;YACD,MAAM,IAAI,aAAa,CAAC,CAAC,0BAA0B;YACnD,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YAC5B,oEAAoE;YACpE,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;YACrC,IAAI,CAAC,aAAa,GAAG,QAAQ,CAAC;YAC9B,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC;YAC3B,OAAO;QACR,CAAC;QAED,iCAAiC;QACjC,IAAI,YAAY,EAAE,CAAC;YAClB,IAAI,MAAM,GAAG,aAAa,CAAC,CAAC,4BAA4B;YACxD,MAAM,IAAI,sBAAsB,CAAC,CAAC,qCAAqC;YACvE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;gBAC1C,IAAI,CAAC,GAAG,CAAC;oBAAE,MAAM,IAAI,MAAM,CAAC;gBAC5B,MAAM,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC;YACvB,CAAC;YACD,MAAM,IAAI,aAAa,CAAC,CAAC,0BAA0B;YACnD,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YAC5B,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;YACrC,IAAI,CAAC,aAAa,GAAG,QAAQ,CAAC;YAC9B,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC;YAC3B,OAAO;QACR,CAAC;QAED,oCAAoC;QACpC,IAAI,YAAY,GAAG,CAAC,CAAC,CAAC;QACtB,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;QACtE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,EAAE,CAAC,EAAE,EAAE,CAAC;YACnC,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAC3E,MAAM,OAAO,GAAG,CAAC,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAEvD,IAAI,OAAO,KAAK,OAAO,EAAE,CAAC;gBACzB,IAAI,YAAY,KAAK,CAAC,CAAC,EAAE,CAAC;oBACzB,YAAY,GAAG,CAAC,CAAC;gBAClB,CAAC;YACF,CAAC;QACF,CAAC;QAED,aAAa;QACb,IAAI,YAAY,KAAK,CAAC,CAAC,EAAE,CAAC;YACzB,OAAO;QACR,CAAC;QAED,gDAAgD;QAChD,oDAAoD;QACpD,kEAAkE;QAClE,wDAAwD;QACxD,MAAM,WAAW,GAAG,IAAI,CAAC,SAAS,GAAG,MAAM,GAAG,CAAC,CAAC;QAChD,IAAI,YAAY,GAAG,WAAW,EAAE,CAAC;YAChC,uDAAuD;YACvD,IAAI,MAAM,GAAG,aAAa,CAAC,CAAC,4BAA4B;YACxD,MAAM,IAAI,sBAAsB,CAAC,CAAC,qCAAqC;YACvE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;gBAC1C,IAAI,CAAC,GAAG,CAAC;oBAAE,MAAM,IAAI,MAAM,CAAC;gBAC5B,MAAM,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC;YACvB,CAAC;YACD,MAAM,IAAI,aAAa,CAAC,CAAC,0BAA0B;YACnD,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YAC5B,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;YACrC,IAAI,CAAC,aAAa,GAAG,QAAQ,CAAC;YAC9B,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC;YAC3B,OAAO;QACR,CAAC;QAED,wCAAwC;QACxC,+DAA+D;QAC/D,IAAI,MAAM,GAAG,aAAa,CAAC,CAAC,4BAA4B;QAExD,oCAAoC;QACpC,MAAM,QAAQ,GAAG,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC;QAC/C,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;YAClB,MAAM,IAAI,QAAQ,QAAQ,GAAG,CAAC,CAAC,YAAY;QAC5C,CAAC;aAAM,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;YACzB,MAAM,IAAI,QAAQ,CAAC,QAAQ,GAAG,CAAC,CAAC,UAAU;QAC3C,CAAC;QAED,MAAM,IAAI,IAAI,CAAC,CAAC,mBAAmB;QAEnC,2EAA2E;QAC3E,0EAA0E;QAC1E,KAAK,IAAI,CAAC,GAAG,YAAY,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACrD,IAAI,CAAC,GAAG,YAAY;gBAAE,MAAM,IAAI,MAAM,CAAC;YACvC,MAAM,IAAI,SAAS,CAAC,CAAC,qBAAqB;YAC1C,MAAM,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;YACzB,MAAM,WAAW,GAAG,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;YAC7C,IAAI,CAAC,WAAW,IAAI,YAAY,CAAC,IAAI,CAAC,GAAG,KAAK,EAAE,CAAC;gBAChD,4CAA4C;gBAC5C,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,cAAc,CAAC,CAAC;gBAC7E,MAAM,SAAS,GAAG;oBACjB,YAAY,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE;oBACtC,mBAAmB,KAAK,EAAE;oBAC1B,QAAQ,CAAC,mBAAmB,YAAY,CAAC,IAAI,CAAC,EAAE;oBAChD,EAAE;oBACF,4BAA4B;oBAC5B,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE,CAAC,IAAI,GAAG,QAAQ,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;oBACnE,EAAE;iBACF,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACb,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;gBAC9D,EAAE,CAAC,aAAa,CAAC,YAAY,EAAE,SAAS,CAAC,CAAC;gBAC1C,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,iDAAiD,YAAY,EAAE,CAAC,CAAC;YACpG,CAAC;YACD,MAAM,IAAI,IAAI,CAAC;QAChB,CAAC;QAED,+DAA+D;QAC/D,IAAI,IAAI,CAAC,aAAa,CAAC,MAAM,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC;YACjD,MAAM,UAAU,GAAG,IAAI,CAAC,aAAa,CAAC,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC;YAC/D,KAAK,IAAI,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC,aAAa,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;gBAClE,MAAM,IAAI,aAAa,CAAC;YACzB,CAAC;YACD,yCAAyC;YACzC,MAAM,IAAI,QAAQ,UAAU,GAAG,CAAC;QACjC,CAAC;QAED,MAAM,IAAI,aAAa,CAAC,CAAC,0BAA0B;QAEnD,8BAA8B;QAC9B,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAE5B,oCAAoC;QACpC,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;QAErC,IAAI,CAAC,aAAa,GAAG,QAAQ,CAAC;QAC9B,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC;IAAA,CAC3B;CACD","sourcesContent":["/**\n * Minimal TUI implementation with differential rendering\n */\n\nimport * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { matchesKey } from \"./keys.js\";\nimport type { Terminal } from \"./terminal.js\";\nimport { getCapabilities, setCellDimensions } from \"./terminal-image.js\";\nimport { visibleWidth } from \"./utils.js\";\n\n/**\n * Component interface - all components must implement this\n */\nexport interface Component {\n\t/**\n\t * Render the component to lines for the given viewport width\n\t * @param width - Current viewport width\n\t * @returns Array of strings, each representing a line\n\t */\n\trender(width: number): string[];\n\n\t/**\n\t * Optional handler for keyboard input when component has focus\n\t */\n\thandleInput?(data: string): void;\n\n\t/**\n\t * Invalidate any cached rendering state.\n\t * Called when theme changes or when component needs to re-render from scratch.\n\t */\n\tinvalidate(): void;\n}\n\nexport { visibleWidth };\n\n/**\n * Container - a component that contains other components\n */\nexport class Container implements Component {\n\tchildren: Component[] = [];\n\n\taddChild(component: Component): void {\n\t\tthis.children.push(component);\n\t}\n\n\tremoveChild(component: Component): void {\n\t\tconst index = this.children.indexOf(component);\n\t\tif (index !== -1) {\n\t\t\tthis.children.splice(index, 1);\n\t\t}\n\t}\n\n\tclear(): void {\n\t\tthis.children = [];\n\t}\n\n\tinvalidate(): void {\n\t\tfor (const child of this.children) {\n\t\t\tchild.invalidate?.();\n\t\t}\n\t}\n\n\trender(width: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tfor (const child of this.children) {\n\t\t\tlines.push(...child.render(width));\n\t\t}\n\t\treturn lines;\n\t}\n}\n\n/**\n * TUI - Main class for managing terminal UI with differential rendering\n */\nexport class TUI extends Container {\n\tpublic terminal: Terminal;\n\tprivate previousLines: string[] = [];\n\tprivate previousWidth = 0;\n\tprivate focusedComponent: Component | null = null;\n\n\t/** Global callback for debug key (Shift+Ctrl+D). Called before input is forwarded to focused component. */\n\tpublic onDebug?: () => void;\n\tprivate renderRequested = false;\n\tprivate cursorRow = 0; // Track where cursor is (0-indexed, relative to our first line)\n\tprivate inputBuffer = \"\"; // Buffer for parsing terminal responses\n\tprivate cellSizeQueryPending = false;\n\n\tconstructor(terminal: Terminal) {\n\t\tsuper();\n\t\tthis.terminal = terminal;\n\t}\n\n\tsetFocus(component: Component | null): void {\n\t\tthis.focusedComponent = component;\n\t}\n\n\tstart(): void {\n\t\tthis.terminal.start(\n\t\t\t(data) => this.handleInput(data),\n\t\t\t() => this.requestRender(),\n\t\t);\n\t\tthis.terminal.hideCursor();\n\t\tthis.queryCellSize();\n\t\tthis.requestRender();\n\t}\n\n\tprivate queryCellSize(): void {\n\t\t// Only query if terminal supports images (cell size is only used for image rendering)\n\t\tif (!getCapabilities().images) {\n\t\t\treturn;\n\t\t}\n\t\t// Query terminal for cell size in pixels: CSI 16 t\n\t\t// Response format: CSI 6 ; height ; width t\n\t\tthis.cellSizeQueryPending = true;\n\t\tthis.terminal.write(\"\\x1b[16t\");\n\t}\n\n\tstop(): void {\n\t\tthis.terminal.showCursor();\n\t\tthis.terminal.stop();\n\t}\n\n\trequestRender(force = false): void {\n\t\tif (force) {\n\t\t\tthis.previousLines = [];\n\t\t\tthis.previousWidth = 0;\n\t\t\tthis.cursorRow = 0;\n\t\t}\n\t\tif (this.renderRequested) return;\n\t\tthis.renderRequested = true;\n\t\tprocess.nextTick(() => {\n\t\t\tthis.renderRequested = false;\n\t\t\tthis.doRender();\n\t\t});\n\t}\n\n\tprivate handleInput(data: string): void {\n\t\t// If we're waiting for cell size response, buffer input and parse\n\t\tif (this.cellSizeQueryPending) {\n\t\t\tthis.inputBuffer += data;\n\t\t\tconst filtered = this.parseCellSizeResponse();\n\t\t\tif (filtered.length === 0) return;\n\t\t\tdata = filtered;\n\t\t}\n\n\t\t// Global debug key handler (Shift+Ctrl+D)\n\t\tif (matchesKey(data, \"shift+ctrl+d\") && this.onDebug) {\n\t\t\tthis.onDebug();\n\t\t\treturn;\n\t\t}\n\n\t\t// Pass input to focused component (including Ctrl+C)\n\t\t// The focused component can decide how to handle Ctrl+C\n\t\tif (this.focusedComponent?.handleInput) {\n\t\t\tthis.focusedComponent.handleInput(data);\n\t\t\tthis.requestRender();\n\t\t}\n\t}\n\n\tprivate parseCellSizeResponse(): string {\n\t\t// Response format: ESC [ 6 ; height ; width t\n\t\t// Match the response pattern\n\t\tconst responsePattern = /\\x1b\\[6;(\\d+);(\\d+)t/;\n\t\tconst match = this.inputBuffer.match(responsePattern);\n\n\t\tif (match) {\n\t\t\tconst heightPx = parseInt(match[1], 10);\n\t\t\tconst widthPx = parseInt(match[2], 10);\n\n\t\t\tif (heightPx > 0 && widthPx > 0) {\n\t\t\t\tsetCellDimensions({ widthPx, heightPx });\n\t\t\t\t// Invalidate all components so images re-render with correct dimensions\n\t\t\t\tthis.invalidate();\n\t\t\t\tthis.requestRender();\n\t\t\t}\n\n\t\t\t// Remove the response from buffer\n\t\t\tthis.inputBuffer = this.inputBuffer.replace(responsePattern, \"\");\n\t\t\tthis.cellSizeQueryPending = false;\n\t\t}\n\n\t\t// Check if we have a partial cell size response starting (wait for more data)\n\t\t// Patterns that could be incomplete cell size response: \\x1b, \\x1b[, \\x1b[6, \\x1b[6;...(no t yet)\n\t\tconst partialCellSizePattern = /\\x1b(\\[6?;?[\\d;]*)?$/;\n\t\tif (partialCellSizePattern.test(this.inputBuffer)) {\n\t\t\t// Check if it's actually a complete different escape sequence (ends with a letter)\n\t\t\t// Cell size response ends with 't', Kitty keyboard ends with 'u', arrows end with A-D, etc.\n\t\t\tconst lastChar = this.inputBuffer[this.inputBuffer.length - 1];\n\t\t\tif (!/[a-zA-Z~]/.test(lastChar)) {\n\t\t\t\t// Doesn't end with a terminator, might be incomplete - wait for more\n\t\t\t\treturn \"\";\n\t\t\t}\n\t\t}\n\n\t\t// No cell size response found, return buffered data as user input\n\t\tconst result = this.inputBuffer;\n\t\tthis.inputBuffer = \"\";\n\t\tthis.cellSizeQueryPending = false; // Give up waiting\n\t\treturn result;\n\t}\n\n\tprivate containsImage(line: string): boolean {\n\t\treturn line.includes(\"\\x1b_G\") || line.includes(\"\\x1b]1337;File=\");\n\t}\n\n\tprivate doRender(): void {\n\t\tconst width = this.terminal.columns;\n\t\tconst height = this.terminal.rows;\n\n\t\t// Render all components to get new lines\n\t\tconst newLines = this.render(width);\n\n\t\t// Width changed - need full re-render\n\t\tconst widthChanged = this.previousWidth !== 0 && this.previousWidth !== width;\n\n\t\t// First render - just output everything without clearing\n\t\tif (this.previousLines.length === 0) {\n\t\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\t\t\tfor (let i = 0; i < newLines.length; i++) {\n\t\t\t\tif (i > 0) buffer += \"\\r\\n\";\n\t\t\t\tbuffer += newLines[i];\n\t\t\t}\n\t\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\t\t\tthis.terminal.write(buffer);\n\t\t\t// After rendering N lines, cursor is at end of last line (line N-1)\n\t\t\tthis.cursorRow = newLines.length - 1;\n\t\t\tthis.previousLines = newLines;\n\t\t\tthis.previousWidth = width;\n\t\t\treturn;\n\t\t}\n\n\t\t// Width changed - full re-render\n\t\tif (widthChanged) {\n\t\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\t\t\tbuffer += \"\\x1b[3J\\x1b[2J\\x1b[H\"; // Clear scrollback, screen, and home\n\t\t\tfor (let i = 0; i < newLines.length; i++) {\n\t\t\t\tif (i > 0) buffer += \"\\r\\n\";\n\t\t\t\tbuffer += newLines[i];\n\t\t\t}\n\t\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\t\t\tthis.terminal.write(buffer);\n\t\t\tthis.cursorRow = newLines.length - 1;\n\t\t\tthis.previousLines = newLines;\n\t\t\tthis.previousWidth = width;\n\t\t\treturn;\n\t\t}\n\n\t\t// Find first and last changed lines\n\t\tlet firstChanged = -1;\n\t\tconst maxLines = Math.max(newLines.length, this.previousLines.length);\n\t\tfor (let i = 0; i < maxLines; i++) {\n\t\t\tconst oldLine = i < this.previousLines.length ? this.previousLines[i] : \"\";\n\t\t\tconst newLine = i < newLines.length ? newLines[i] : \"\";\n\n\t\t\tif (oldLine !== newLine) {\n\t\t\t\tif (firstChanged === -1) {\n\t\t\t\t\tfirstChanged = i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// No changes\n\t\tif (firstChanged === -1) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Check if firstChanged is outside the viewport\n\t\t// cursorRow is the line where cursor is (0-indexed)\n\t\t// Viewport shows lines from (cursorRow - height + 1) to cursorRow\n\t\t// If firstChanged < viewportTop, we need full re-render\n\t\tconst viewportTop = this.cursorRow - height + 1;\n\t\tif (firstChanged < viewportTop) {\n\t\t\t// First change is above viewport - need full re-render\n\t\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\t\t\tbuffer += \"\\x1b[3J\\x1b[2J\\x1b[H\"; // Clear scrollback, screen, and home\n\t\t\tfor (let i = 0; i < newLines.length; i++) {\n\t\t\t\tif (i > 0) buffer += \"\\r\\n\";\n\t\t\t\tbuffer += newLines[i];\n\t\t\t}\n\t\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\t\t\tthis.terminal.write(buffer);\n\t\t\tthis.cursorRow = newLines.length - 1;\n\t\t\tthis.previousLines = newLines;\n\t\t\tthis.previousWidth = width;\n\t\t\treturn;\n\t\t}\n\n\t\t// Render from first changed line to end\n\t\t// Build buffer with all updates wrapped in synchronized output\n\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\n\t\t// Move cursor to first changed line\n\t\tconst lineDiff = firstChanged - this.cursorRow;\n\t\tif (lineDiff > 0) {\n\t\t\tbuffer += `\\x1b[${lineDiff}B`; // Move down\n\t\t} else if (lineDiff < 0) {\n\t\t\tbuffer += `\\x1b[${-lineDiff}A`; // Move up\n\t\t}\n\n\t\tbuffer += \"\\r\"; // Move to column 0\n\n\t\t// Render from first changed line to end, clearing each line before writing\n\t\t// This avoids the \\x1b[J clear-to-end which can cause flicker in xterm.js\n\t\tfor (let i = firstChanged; i < newLines.length; i++) {\n\t\t\tif (i > firstChanged) buffer += \"\\r\\n\";\n\t\t\tbuffer += \"\\x1b[2K\"; // Clear current line\n\t\t\tconst line = newLines[i];\n\t\t\tconst isImageLine = this.containsImage(line);\n\t\t\tif (!isImageLine && visibleWidth(line) > width) {\n\t\t\t\t// Log all lines to crash file for debugging\n\t\t\t\tconst crashLogPath = path.join(os.homedir(), \".pi\", \"agent\", \"pi-crash.log\");\n\t\t\t\tconst crashData = [\n\t\t\t\t\t`Crash at ${new Date().toISOString()}`,\n\t\t\t\t\t`Terminal width: ${width}`,\n\t\t\t\t\t`Line ${i} visible width: ${visibleWidth(line)}`,\n\t\t\t\t\t\"\",\n\t\t\t\t\t\"=== All rendered lines ===\",\n\t\t\t\t\t...newLines.map((l, idx) => `[${idx}] (w=${visibleWidth(l)}) ${l}`),\n\t\t\t\t\t\"\",\n\t\t\t\t].join(\"\\n\");\n\t\t\t\tfs.mkdirSync(path.dirname(crashLogPath), { recursive: true });\n\t\t\t\tfs.writeFileSync(crashLogPath, crashData);\n\t\t\t\tthrow new Error(`Rendered line ${i} exceeds terminal width. Debug log written to ${crashLogPath}`);\n\t\t\t}\n\t\t\tbuffer += line;\n\t\t}\n\n\t\t// If we had more lines before, clear them and move cursor back\n\t\tif (this.previousLines.length > newLines.length) {\n\t\t\tconst extraLines = this.previousLines.length - newLines.length;\n\t\t\tfor (let i = newLines.length; i < this.previousLines.length; i++) {\n\t\t\t\tbuffer += \"\\r\\n\\x1b[2K\";\n\t\t\t}\n\t\t\t// Move cursor back to end of new content\n\t\t\tbuffer += `\\x1b[${extraLines}A`;\n\t\t}\n\n\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\n\t\t// Write entire buffer at once\n\t\tthis.terminal.write(buffer);\n\n\t\t// Cursor is now at end of last line\n\t\tthis.cursorRow = newLines.length - 1;\n\n\t\tthis.previousLines = newLines;\n\t\tthis.previousWidth = width;\n\t}\n}\n"]}
|
|
1
|
+
{"version":3,"file":"tui.js","sourceRoot":"","sources":["../src/tui.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,EAAE,MAAM,SAAS,CAAC;AAC9B,OAAO,KAAK,IAAI,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AAErD,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,qBAAqB,CAAC;AACzE,OAAO,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AA+B1C,OAAO,EAAE,YAAY,EAAE,CAAC;AAExB;;GAEG;AACH,MAAM,OAAO,SAAS;IACrB,QAAQ,GAAgB,EAAE,CAAC;IAE3B,QAAQ,CAAC,SAAoB,EAAQ;QACpC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAAA,CAC9B;IAED,WAAW,CAAC,SAAoB,EAAQ;QACvC,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAC/C,IAAI,KAAK,KAAK,CAAC,CAAC,EAAE,CAAC;YAClB,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;QAChC,CAAC;IAAA,CACD;IAED,KAAK,GAAS;QACb,IAAI,CAAC,QAAQ,GAAG,EAAE,CAAC;IAAA,CACnB;IAED,UAAU,GAAS;QAClB,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnC,KAAK,CAAC,UAAU,EAAE,EAAE,CAAC;QACtB,CAAC;IAAA,CACD;IAED,MAAM,CAAC,KAAa,EAAY;QAC/B,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnC,KAAK,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACpC,CAAC;QACD,OAAO,KAAK,CAAC;IAAA,CACb;CACD;AAED;;GAEG;AACH,MAAM,OAAO,GAAI,SAAQ,SAAS;IAC1B,QAAQ,CAAW;IAClB,aAAa,GAAa,EAAE,CAAC;IAC7B,aAAa,GAAG,CAAC,CAAC;IAClB,gBAAgB,GAAqB,IAAI,CAAC;IAElD,2GAA2G;IACpG,OAAO,CAAc;IACpB,eAAe,GAAG,KAAK,CAAC;IACxB,SAAS,GAAG,CAAC,CAAC,CAAC,gEAAgE;IAC/E,WAAW,GAAG,EAAE,CAAC,CAAC,wCAAwC;IAC1D,oBAAoB,GAAG,KAAK,CAAC;IAErC,YAAY,QAAkB,EAAE;QAC/B,KAAK,EAAE,CAAC;QACR,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;IAAA,CACzB;IAED,QAAQ,CAAC,SAA2B,EAAQ;QAC3C,IAAI,CAAC,gBAAgB,GAAG,SAAS,CAAC;IAAA,CAClC;IAED,KAAK,GAAS;QACb,IAAI,CAAC,QAAQ,CAAC,KAAK,CAClB,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,EAChC,GAAG,EAAE,CAAC,IAAI,CAAC,aAAa,EAAE,CAC1B,CAAC;QACF,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,CAAC;QAC3B,IAAI,CAAC,aAAa,EAAE,CAAC;QACrB,IAAI,CAAC,aAAa,EAAE,CAAC;IAAA,CACrB;IAEO,aAAa,GAAS;QAC7B,sFAAsF;QACtF,IAAI,CAAC,eAAe,EAAE,CAAC,MAAM,EAAE,CAAC;YAC/B,OAAO;QACR,CAAC;QACD,mDAAmD;QACnD,4CAA4C;QAC5C,IAAI,CAAC,oBAAoB,GAAG,IAAI,CAAC;QACjC,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;IAAA,CAChC;IAED,IAAI,GAAS;QACZ,IAAI,CAAC,QAAQ,CAAC,UAAU,EAAE,CAAC;QAC3B,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;IAAA,CACrB;IAED,aAAa,CAAC,KAAK,GAAG,KAAK,EAAQ;QAClC,IAAI,KAAK,EAAE,CAAC;YACX,IAAI,CAAC,aAAa,GAAG,EAAE,CAAC;YACxB,IAAI,CAAC,aAAa,GAAG,CAAC,CAAC;YACvB,IAAI,CAAC,SAAS,GAAG,CAAC,CAAC;QACpB,CAAC;QACD,IAAI,IAAI,CAAC,eAAe;YAAE,OAAO;QACjC,IAAI,CAAC,eAAe,GAAG,IAAI,CAAC;QAC5B,OAAO,CAAC,QAAQ,CAAC,GAAG,EAAE,CAAC;YACtB,IAAI,CAAC,eAAe,GAAG,KAAK,CAAC;YAC7B,IAAI,CAAC,QAAQ,EAAE,CAAC;QAAA,CAChB,CAAC,CAAC;IAAA,CACH;IAEO,WAAW,CAAC,IAAY,EAAQ;QACvC,kEAAkE;QAClE,IAAI,IAAI,CAAC,oBAAoB,EAAE,CAAC;YAC/B,IAAI,CAAC,WAAW,IAAI,IAAI,CAAC;YACzB,MAAM,QAAQ,GAAG,IAAI,CAAC,qBAAqB,EAAE,CAAC;YAC9C,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO;YAClC,IAAI,GAAG,QAAQ,CAAC;QACjB,CAAC;QAED,0CAA0C;QAC1C,IAAI,UAAU,CAAC,IAAI,EAAE,cAAc,CAAC,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YACtD,IAAI,CAAC,OAAO,EAAE,CAAC;YACf,OAAO;QACR,CAAC;QAED,qDAAqD;QACrD,wDAAwD;QACxD,IAAI,IAAI,CAAC,gBAAgB,EAAE,WAAW,EAAE,CAAC;YACxC,yDAAyD;YACzD,IAAI,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,gBAAgB,CAAC,eAAe,EAAE,CAAC;gBAClE,OAAO;YACR,CAAC;YACD,IAAI,CAAC,gBAAgB,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;YACxC,IAAI,CAAC,aAAa,EAAE,CAAC;QACtB,CAAC;IAAA,CACD;IAEO,qBAAqB,GAAW;QACvC,8CAA8C;QAC9C,6BAA6B;QAC7B,MAAM,eAAe,GAAG,sBAAsB,CAAC;QAC/C,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;QAEtD,IAAI,KAAK,EAAE,CAAC;YACX,MAAM,QAAQ,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACxC,MAAM,OAAO,GAAG,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YAEvC,IAAI,QAAQ,GAAG,CAAC,IAAI,OAAO,GAAG,CAAC,EAAE,CAAC;gBACjC,iBAAiB,CAAC,EAAE,OAAO,EAAE,QAAQ,EAAE,CAAC,CAAC;gBACzC,wEAAwE;gBACxE,IAAI,CAAC,UAAU,EAAE,CAAC;gBAClB,IAAI,CAAC,aAAa,EAAE,CAAC;YACtB,CAAC;YAED,kCAAkC;YAClC,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,WAAW,CAAC,OAAO,CAAC,eAAe,EAAE,EAAE,CAAC,CAAC;YACjE,IAAI,CAAC,oBAAoB,GAAG,KAAK,CAAC;QACnC,CAAC;QAED,8EAA8E;QAC9E,kGAAkG;QAClG,MAAM,sBAAsB,GAAG,sBAAsB,CAAC;QACtD,IAAI,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;YACnD,mFAAmF;YACnF,4FAA4F;YAC5F,MAAM,QAAQ,GAAG,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;YAC/D,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;gBACjC,qEAAqE;gBACrE,OAAO,EAAE,CAAC;YACX,CAAC;QACF,CAAC;QAED,kEAAkE;QAClE,MAAM,MAAM,GAAG,IAAI,CAAC,WAAW,CAAC;QAChC,IAAI,CAAC,WAAW,GAAG,EAAE,CAAC;QACtB,IAAI,CAAC,oBAAoB,GAAG,KAAK,CAAC,CAAC,kBAAkB;QACrD,OAAO,MAAM,CAAC;IAAA,CACd;IAEO,aAAa,CAAC,IAAY,EAAW;QAC5C,OAAO,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,iBAAiB,CAAC,CAAC;IAAA,CACnE;IAEO,QAAQ,GAAS;QACxB,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC;QACpC,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;QAElC,yCAAyC;QACzC,MAAM,QAAQ,GAAG,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QAEpC,sCAAsC;QACtC,MAAM,YAAY,GAAG,IAAI,CAAC,aAAa,KAAK,CAAC,IAAI,IAAI,CAAC,aAAa,KAAK,KAAK,CAAC;QAE9E,yDAAyD;QACzD,IAAI,IAAI,CAAC,aAAa,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACrC,IAAI,MAAM,GAAG,aAAa,CAAC,CAAC,4BAA4B;YACxD,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;gBAC1C,IAAI,CAAC,GAAG,CAAC;oBAAE,MAAM,IAAI,MAAM,CAAC;gBAC5B,MAAM,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC;YACvB,CAAC;YACD,MAAM,IAAI,aAAa,CAAC,CAAC,0BAA0B;YACnD,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YAC5B,oEAAoE;YACpE,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;YACrC,IAAI,CAAC,aAAa,GAAG,QAAQ,CAAC;YAC9B,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC;YAC3B,OAAO;QACR,CAAC;QAED,iCAAiC;QACjC,IAAI,YAAY,EAAE,CAAC;YAClB,IAAI,MAAM,GAAG,aAAa,CAAC,CAAC,4BAA4B;YACxD,MAAM,IAAI,sBAAsB,CAAC,CAAC,qCAAqC;YACvE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;gBAC1C,IAAI,CAAC,GAAG,CAAC;oBAAE,MAAM,IAAI,MAAM,CAAC;gBAC5B,MAAM,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC;YACvB,CAAC;YACD,MAAM,IAAI,aAAa,CAAC,CAAC,0BAA0B;YACnD,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YAC5B,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;YACrC,IAAI,CAAC,aAAa,GAAG,QAAQ,CAAC;YAC9B,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC;YAC3B,OAAO;QACR,CAAC;QAED,oCAAoC;QACpC,IAAI,YAAY,GAAG,CAAC,CAAC,CAAC;QACtB,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;QACtE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,EAAE,CAAC,EAAE,EAAE,CAAC;YACnC,MAAM,OAAO,GAAG,CAAC,GAAG,IAAI,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAC3E,MAAM,OAAO,GAAG,CAAC,GAAG,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;YAEvD,IAAI,OAAO,KAAK,OAAO,EAAE,CAAC;gBACzB,IAAI,YAAY,KAAK,CAAC,CAAC,EAAE,CAAC;oBACzB,YAAY,GAAG,CAAC,CAAC;gBAClB,CAAC;YACF,CAAC;QACF,CAAC;QAED,aAAa;QACb,IAAI,YAAY,KAAK,CAAC,CAAC,EAAE,CAAC;YACzB,OAAO;QACR,CAAC;QAED,gDAAgD;QAChD,oDAAoD;QACpD,kEAAkE;QAClE,wDAAwD;QACxD,MAAM,WAAW,GAAG,IAAI,CAAC,SAAS,GAAG,MAAM,GAAG,CAAC,CAAC;QAChD,IAAI,YAAY,GAAG,WAAW,EAAE,CAAC;YAChC,uDAAuD;YACvD,IAAI,MAAM,GAAG,aAAa,CAAC,CAAC,4BAA4B;YACxD,MAAM,IAAI,sBAAsB,CAAC,CAAC,qCAAqC;YACvE,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;gBAC1C,IAAI,CAAC,GAAG,CAAC;oBAAE,MAAM,IAAI,MAAM,CAAC;gBAC5B,MAAM,IAAI,QAAQ,CAAC,CAAC,CAAC,CAAC;YACvB,CAAC;YACD,MAAM,IAAI,aAAa,CAAC,CAAC,0BAA0B;YACnD,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;YAC5B,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;YACrC,IAAI,CAAC,aAAa,GAAG,QAAQ,CAAC;YAC9B,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC;YAC3B,OAAO;QACR,CAAC;QAED,wCAAwC;QACxC,+DAA+D;QAC/D,IAAI,MAAM,GAAG,aAAa,CAAC,CAAC,4BAA4B;QAExD,oCAAoC;QACpC,MAAM,QAAQ,GAAG,YAAY,GAAG,IAAI,CAAC,SAAS,CAAC;QAC/C,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;YAClB,MAAM,IAAI,QAAQ,QAAQ,GAAG,CAAC,CAAC,YAAY;QAC5C,CAAC;aAAM,IAAI,QAAQ,GAAG,CAAC,EAAE,CAAC;YACzB,MAAM,IAAI,QAAQ,CAAC,QAAQ,GAAG,CAAC,CAAC,UAAU;QAC3C,CAAC;QAED,MAAM,IAAI,IAAI,CAAC,CAAC,mBAAmB;QAEnC,2EAA2E;QAC3E,0EAA0E;QAC1E,KAAK,IAAI,CAAC,GAAG,YAAY,EAAE,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACrD,IAAI,CAAC,GAAG,YAAY;gBAAE,MAAM,IAAI,MAAM,CAAC;YACvC,MAAM,IAAI,SAAS,CAAC,CAAC,qBAAqB;YAC1C,MAAM,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;YACzB,MAAM,WAAW,GAAG,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC;YAC7C,IAAI,CAAC,WAAW,IAAI,YAAY,CAAC,IAAI,CAAC,GAAG,KAAK,EAAE,CAAC;gBAChD,4CAA4C;gBAC5C,MAAM,YAAY,GAAG,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,cAAc,CAAC,CAAC;gBAC7E,MAAM,SAAS,GAAG;oBACjB,YAAY,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,EAAE;oBACtC,mBAAmB,KAAK,EAAE;oBAC1B,QAAQ,CAAC,mBAAmB,YAAY,CAAC,IAAI,CAAC,EAAE;oBAChD,EAAE;oBACF,4BAA4B;oBAC5B,GAAG,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,EAAE,CAAC,IAAI,GAAG,QAAQ,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,EAAE,CAAC;oBACnE,EAAE;iBACF,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;gBACb,EAAE,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,YAAY,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;gBAC9D,EAAE,CAAC,aAAa,CAAC,YAAY,EAAE,SAAS,CAAC,CAAC;gBAC1C,MAAM,IAAI,KAAK,CAAC,iBAAiB,CAAC,iDAAiD,YAAY,EAAE,CAAC,CAAC;YACpG,CAAC;YACD,MAAM,IAAI,IAAI,CAAC;QAChB,CAAC;QAED,+DAA+D;QAC/D,IAAI,IAAI,CAAC,aAAa,CAAC,MAAM,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC;YACjD,MAAM,UAAU,GAAG,IAAI,CAAC,aAAa,CAAC,MAAM,GAAG,QAAQ,CAAC,MAAM,CAAC;YAC/D,KAAK,IAAI,CAAC,GAAG,QAAQ,CAAC,MAAM,EAAE,CAAC,GAAG,IAAI,CAAC,aAAa,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;gBAClE,MAAM,IAAI,aAAa,CAAC;YACzB,CAAC;YACD,yCAAyC;YACzC,MAAM,IAAI,QAAQ,UAAU,GAAG,CAAC;QACjC,CAAC;QAED,MAAM,IAAI,aAAa,CAAC,CAAC,0BAA0B;QAEnD,8BAA8B;QAC9B,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAE5B,oCAAoC;QACpC,IAAI,CAAC,SAAS,GAAG,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC;QAErC,IAAI,CAAC,aAAa,GAAG,QAAQ,CAAC;QAC9B,IAAI,CAAC,aAAa,GAAG,KAAK,CAAC;IAAA,CAC3B;CACD","sourcesContent":["/**\n * Minimal TUI implementation with differential rendering\n */\n\nimport * as fs from \"node:fs\";\nimport * as os from \"node:os\";\nimport * as path from \"node:path\";\nimport { isKeyRelease, matchesKey } from \"./keys.js\";\nimport type { Terminal } from \"./terminal.js\";\nimport { getCapabilities, setCellDimensions } from \"./terminal-image.js\";\nimport { visibleWidth } from \"./utils.js\";\n\n/**\n * Component interface - all components must implement this\n */\nexport interface Component {\n\t/**\n\t * Render the component to lines for the given viewport width\n\t * @param width - Current viewport width\n\t * @returns Array of strings, each representing a line\n\t */\n\trender(width: number): string[];\n\n\t/**\n\t * Optional handler for keyboard input when component has focus\n\t */\n\thandleInput?(data: string): void;\n\n\t/**\n\t * If true, component receives key release events (Kitty protocol).\n\t * Default is false - release events are filtered out.\n\t */\n\twantsKeyRelease?: boolean;\n\n\t/**\n\t * Invalidate any cached rendering state.\n\t * Called when theme changes or when component needs to re-render from scratch.\n\t */\n\tinvalidate(): void;\n}\n\nexport { visibleWidth };\n\n/**\n * Container - a component that contains other components\n */\nexport class Container implements Component {\n\tchildren: Component[] = [];\n\n\taddChild(component: Component): void {\n\t\tthis.children.push(component);\n\t}\n\n\tremoveChild(component: Component): void {\n\t\tconst index = this.children.indexOf(component);\n\t\tif (index !== -1) {\n\t\t\tthis.children.splice(index, 1);\n\t\t}\n\t}\n\n\tclear(): void {\n\t\tthis.children = [];\n\t}\n\n\tinvalidate(): void {\n\t\tfor (const child of this.children) {\n\t\t\tchild.invalidate?.();\n\t\t}\n\t}\n\n\trender(width: number): string[] {\n\t\tconst lines: string[] = [];\n\t\tfor (const child of this.children) {\n\t\t\tlines.push(...child.render(width));\n\t\t}\n\t\treturn lines;\n\t}\n}\n\n/**\n * TUI - Main class for managing terminal UI with differential rendering\n */\nexport class TUI extends Container {\n\tpublic terminal: Terminal;\n\tprivate previousLines: string[] = [];\n\tprivate previousWidth = 0;\n\tprivate focusedComponent: Component | null = null;\n\n\t/** Global callback for debug key (Shift+Ctrl+D). Called before input is forwarded to focused component. */\n\tpublic onDebug?: () => void;\n\tprivate renderRequested = false;\n\tprivate cursorRow = 0; // Track where cursor is (0-indexed, relative to our first line)\n\tprivate inputBuffer = \"\"; // Buffer for parsing terminal responses\n\tprivate cellSizeQueryPending = false;\n\n\tconstructor(terminal: Terminal) {\n\t\tsuper();\n\t\tthis.terminal = terminal;\n\t}\n\n\tsetFocus(component: Component | null): void {\n\t\tthis.focusedComponent = component;\n\t}\n\n\tstart(): void {\n\t\tthis.terminal.start(\n\t\t\t(data) => this.handleInput(data),\n\t\t\t() => this.requestRender(),\n\t\t);\n\t\tthis.terminal.hideCursor();\n\t\tthis.queryCellSize();\n\t\tthis.requestRender();\n\t}\n\n\tprivate queryCellSize(): void {\n\t\t// Only query if terminal supports images (cell size is only used for image rendering)\n\t\tif (!getCapabilities().images) {\n\t\t\treturn;\n\t\t}\n\t\t// Query terminal for cell size in pixels: CSI 16 t\n\t\t// Response format: CSI 6 ; height ; width t\n\t\tthis.cellSizeQueryPending = true;\n\t\tthis.terminal.write(\"\\x1b[16t\");\n\t}\n\n\tstop(): void {\n\t\tthis.terminal.showCursor();\n\t\tthis.terminal.stop();\n\t}\n\n\trequestRender(force = false): void {\n\t\tif (force) {\n\t\t\tthis.previousLines = [];\n\t\t\tthis.previousWidth = 0;\n\t\t\tthis.cursorRow = 0;\n\t\t}\n\t\tif (this.renderRequested) return;\n\t\tthis.renderRequested = true;\n\t\tprocess.nextTick(() => {\n\t\t\tthis.renderRequested = false;\n\t\t\tthis.doRender();\n\t\t});\n\t}\n\n\tprivate handleInput(data: string): void {\n\t\t// If we're waiting for cell size response, buffer input and parse\n\t\tif (this.cellSizeQueryPending) {\n\t\t\tthis.inputBuffer += data;\n\t\t\tconst filtered = this.parseCellSizeResponse();\n\t\t\tif (filtered.length === 0) return;\n\t\t\tdata = filtered;\n\t\t}\n\n\t\t// Global debug key handler (Shift+Ctrl+D)\n\t\tif (matchesKey(data, \"shift+ctrl+d\") && this.onDebug) {\n\t\t\tthis.onDebug();\n\t\t\treturn;\n\t\t}\n\n\t\t// Pass input to focused component (including Ctrl+C)\n\t\t// The focused component can decide how to handle Ctrl+C\n\t\tif (this.focusedComponent?.handleInput) {\n\t\t\t// Filter out key release events unless component opts in\n\t\t\tif (isKeyRelease(data) && !this.focusedComponent.wantsKeyRelease) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tthis.focusedComponent.handleInput(data);\n\t\t\tthis.requestRender();\n\t\t}\n\t}\n\n\tprivate parseCellSizeResponse(): string {\n\t\t// Response format: ESC [ 6 ; height ; width t\n\t\t// Match the response pattern\n\t\tconst responsePattern = /\\x1b\\[6;(\\d+);(\\d+)t/;\n\t\tconst match = this.inputBuffer.match(responsePattern);\n\n\t\tif (match) {\n\t\t\tconst heightPx = parseInt(match[1], 10);\n\t\t\tconst widthPx = parseInt(match[2], 10);\n\n\t\t\tif (heightPx > 0 && widthPx > 0) {\n\t\t\t\tsetCellDimensions({ widthPx, heightPx });\n\t\t\t\t// Invalidate all components so images re-render with correct dimensions\n\t\t\t\tthis.invalidate();\n\t\t\t\tthis.requestRender();\n\t\t\t}\n\n\t\t\t// Remove the response from buffer\n\t\t\tthis.inputBuffer = this.inputBuffer.replace(responsePattern, \"\");\n\t\t\tthis.cellSizeQueryPending = false;\n\t\t}\n\n\t\t// Check if we have a partial cell size response starting (wait for more data)\n\t\t// Patterns that could be incomplete cell size response: \\x1b, \\x1b[, \\x1b[6, \\x1b[6;...(no t yet)\n\t\tconst partialCellSizePattern = /\\x1b(\\[6?;?[\\d;]*)?$/;\n\t\tif (partialCellSizePattern.test(this.inputBuffer)) {\n\t\t\t// Check if it's actually a complete different escape sequence (ends with a letter)\n\t\t\t// Cell size response ends with 't', Kitty keyboard ends with 'u', arrows end with A-D, etc.\n\t\t\tconst lastChar = this.inputBuffer[this.inputBuffer.length - 1];\n\t\t\tif (!/[a-zA-Z~]/.test(lastChar)) {\n\t\t\t\t// Doesn't end with a terminator, might be incomplete - wait for more\n\t\t\t\treturn \"\";\n\t\t\t}\n\t\t}\n\n\t\t// No cell size response found, return buffered data as user input\n\t\tconst result = this.inputBuffer;\n\t\tthis.inputBuffer = \"\";\n\t\tthis.cellSizeQueryPending = false; // Give up waiting\n\t\treturn result;\n\t}\n\n\tprivate containsImage(line: string): boolean {\n\t\treturn line.includes(\"\\x1b_G\") || line.includes(\"\\x1b]1337;File=\");\n\t}\n\n\tprivate doRender(): void {\n\t\tconst width = this.terminal.columns;\n\t\tconst height = this.terminal.rows;\n\n\t\t// Render all components to get new lines\n\t\tconst newLines = this.render(width);\n\n\t\t// Width changed - need full re-render\n\t\tconst widthChanged = this.previousWidth !== 0 && this.previousWidth !== width;\n\n\t\t// First render - just output everything without clearing\n\t\tif (this.previousLines.length === 0) {\n\t\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\t\t\tfor (let i = 0; i < newLines.length; i++) {\n\t\t\t\tif (i > 0) buffer += \"\\r\\n\";\n\t\t\t\tbuffer += newLines[i];\n\t\t\t}\n\t\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\t\t\tthis.terminal.write(buffer);\n\t\t\t// After rendering N lines, cursor is at end of last line (line N-1)\n\t\t\tthis.cursorRow = newLines.length - 1;\n\t\t\tthis.previousLines = newLines;\n\t\t\tthis.previousWidth = width;\n\t\t\treturn;\n\t\t}\n\n\t\t// Width changed - full re-render\n\t\tif (widthChanged) {\n\t\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\t\t\tbuffer += \"\\x1b[3J\\x1b[2J\\x1b[H\"; // Clear scrollback, screen, and home\n\t\t\tfor (let i = 0; i < newLines.length; i++) {\n\t\t\t\tif (i > 0) buffer += \"\\r\\n\";\n\t\t\t\tbuffer += newLines[i];\n\t\t\t}\n\t\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\t\t\tthis.terminal.write(buffer);\n\t\t\tthis.cursorRow = newLines.length - 1;\n\t\t\tthis.previousLines = newLines;\n\t\t\tthis.previousWidth = width;\n\t\t\treturn;\n\t\t}\n\n\t\t// Find first and last changed lines\n\t\tlet firstChanged = -1;\n\t\tconst maxLines = Math.max(newLines.length, this.previousLines.length);\n\t\tfor (let i = 0; i < maxLines; i++) {\n\t\t\tconst oldLine = i < this.previousLines.length ? this.previousLines[i] : \"\";\n\t\t\tconst newLine = i < newLines.length ? newLines[i] : \"\";\n\n\t\t\tif (oldLine !== newLine) {\n\t\t\t\tif (firstChanged === -1) {\n\t\t\t\t\tfirstChanged = i;\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// No changes\n\t\tif (firstChanged === -1) {\n\t\t\treturn;\n\t\t}\n\n\t\t// Check if firstChanged is outside the viewport\n\t\t// cursorRow is the line where cursor is (0-indexed)\n\t\t// Viewport shows lines from (cursorRow - height + 1) to cursorRow\n\t\t// If firstChanged < viewportTop, we need full re-render\n\t\tconst viewportTop = this.cursorRow - height + 1;\n\t\tif (firstChanged < viewportTop) {\n\t\t\t// First change is above viewport - need full re-render\n\t\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\t\t\tbuffer += \"\\x1b[3J\\x1b[2J\\x1b[H\"; // Clear scrollback, screen, and home\n\t\t\tfor (let i = 0; i < newLines.length; i++) {\n\t\t\t\tif (i > 0) buffer += \"\\r\\n\";\n\t\t\t\tbuffer += newLines[i];\n\t\t\t}\n\t\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\t\t\tthis.terminal.write(buffer);\n\t\t\tthis.cursorRow = newLines.length - 1;\n\t\t\tthis.previousLines = newLines;\n\t\t\tthis.previousWidth = width;\n\t\t\treturn;\n\t\t}\n\n\t\t// Render from first changed line to end\n\t\t// Build buffer with all updates wrapped in synchronized output\n\t\tlet buffer = \"\\x1b[?2026h\"; // Begin synchronized output\n\n\t\t// Move cursor to first changed line\n\t\tconst lineDiff = firstChanged - this.cursorRow;\n\t\tif (lineDiff > 0) {\n\t\t\tbuffer += `\\x1b[${lineDiff}B`; // Move down\n\t\t} else if (lineDiff < 0) {\n\t\t\tbuffer += `\\x1b[${-lineDiff}A`; // Move up\n\t\t}\n\n\t\tbuffer += \"\\r\"; // Move to column 0\n\n\t\t// Render from first changed line to end, clearing each line before writing\n\t\t// This avoids the \\x1b[J clear-to-end which can cause flicker in xterm.js\n\t\tfor (let i = firstChanged; i < newLines.length; i++) {\n\t\t\tif (i > firstChanged) buffer += \"\\r\\n\";\n\t\t\tbuffer += \"\\x1b[2K\"; // Clear current line\n\t\t\tconst line = newLines[i];\n\t\t\tconst isImageLine = this.containsImage(line);\n\t\t\tif (!isImageLine && visibleWidth(line) > width) {\n\t\t\t\t// Log all lines to crash file for debugging\n\t\t\t\tconst crashLogPath = path.join(os.homedir(), \".pi\", \"agent\", \"pi-crash.log\");\n\t\t\t\tconst crashData = [\n\t\t\t\t\t`Crash at ${new Date().toISOString()}`,\n\t\t\t\t\t`Terminal width: ${width}`,\n\t\t\t\t\t`Line ${i} visible width: ${visibleWidth(line)}`,\n\t\t\t\t\t\"\",\n\t\t\t\t\t\"=== All rendered lines ===\",\n\t\t\t\t\t...newLines.map((l, idx) => `[${idx}] (w=${visibleWidth(l)}) ${l}`),\n\t\t\t\t\t\"\",\n\t\t\t\t].join(\"\\n\");\n\t\t\t\tfs.mkdirSync(path.dirname(crashLogPath), { recursive: true });\n\t\t\t\tfs.writeFileSync(crashLogPath, crashData);\n\t\t\t\tthrow new Error(`Rendered line ${i} exceeds terminal width. Debug log written to ${crashLogPath}`);\n\t\t\t}\n\t\t\tbuffer += line;\n\t\t}\n\n\t\t// If we had more lines before, clear them and move cursor back\n\t\tif (this.previousLines.length > newLines.length) {\n\t\t\tconst extraLines = this.previousLines.length - newLines.length;\n\t\t\tfor (let i = newLines.length; i < this.previousLines.length; i++) {\n\t\t\t\tbuffer += \"\\r\\n\\x1b[2K\";\n\t\t\t}\n\t\t\t// Move cursor back to end of new content\n\t\t\tbuffer += `\\x1b[${extraLines}A`;\n\t\t}\n\n\t\tbuffer += \"\\x1b[?2026l\"; // End synchronized output\n\n\t\t// Write entire buffer at once\n\t\tthis.terminal.write(buffer);\n\n\t\t// Cursor is now at end of last line\n\t\tthis.cursorRow = newLines.length - 1;\n\n\t\tthis.previousLines = newLines;\n\t\tthis.previousWidth = width;\n\t}\n}\n"]}
|
package/package.json
CHANGED