@vybestack/llxprt-ui 0.7.0-nightly.251213.e273961ea → 0.7.0-nightly.251215.66bf0bc39

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.
@@ -0,0 +1,99 @@
1
+ import { readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { createCliRenderer } from '@vybestack/opentui-core';
5
+ import {
6
+ BoxRenderable,
7
+ ImageRenderable,
8
+ TextRenderable,
9
+ } from '@vybestack/opentui-core';
10
+
11
+ const __filename = fileURLToPath(import.meta.url);
12
+ const __dirname = path.dirname(__filename);
13
+
14
+ type CliRenderer = Awaited<ReturnType<typeof createCliRenderer>>;
15
+
16
+ function formatError(err: unknown): string {
17
+ if (err instanceof Error) {
18
+ return err.stack ?? err.message;
19
+ }
20
+
21
+ return String(err);
22
+ }
23
+
24
+ async function main(): Promise<void> {
25
+ const logoPath = path.resolve(__dirname, '../llxprt.png');
26
+ let logo: Buffer;
27
+
28
+ try {
29
+ logo = readFileSync(logoPath);
30
+ } catch (err) {
31
+ throw new Error(`Failed to read logo: ${logoPath}\n${formatError(err)}`);
32
+ }
33
+
34
+ const bg = '#fafafa';
35
+ const border = '#2d7d46';
36
+ const fg = '#222222';
37
+
38
+ let renderer: CliRenderer | undefined;
39
+ try {
40
+ renderer = await createCliRenderer({
41
+ useAlternateScreen: false,
42
+ exitOnCtrlC: false,
43
+ });
44
+
45
+ const header = new BoxRenderable(renderer, {
46
+ id: 'header',
47
+ flexDirection: 'row',
48
+ alignItems: 'center',
49
+ justifyContent: 'flex-start',
50
+ border: true,
51
+ height: 3,
52
+ minHeight: 3,
53
+ maxHeight: 3,
54
+ paddingTop: 0,
55
+ paddingBottom: 0,
56
+ paddingLeft: 1,
57
+ paddingRight: 1,
58
+ borderColor: border,
59
+ backgroundColor: bg,
60
+ });
61
+
62
+ const logoAspectRatio = 415 / 260;
63
+ const image = new ImageRenderable(renderer, {
64
+ id: 'logo',
65
+ src: logo,
66
+ height: 1,
67
+ aspectRatio: logoAspectRatio,
68
+ fit: 'contain',
69
+ backgroundColor: bg,
70
+ });
71
+
72
+ const title = new TextRenderable(renderer, {
73
+ id: 'title',
74
+ content: "LLxprt Code - I'm here to help",
75
+ marginLeft: 1,
76
+ fg,
77
+ });
78
+
79
+ header.add(image);
80
+ header.add(title);
81
+ renderer.root.add(header);
82
+
83
+ renderer.start();
84
+
85
+ // Give pixel resolution a moment to arrive and settle any re-layout.
86
+ await new Promise((r) => setTimeout(r, 1500));
87
+ renderer.pause();
88
+
89
+ // Keep the frame visible for capture.
90
+ await new Promise((r) => setTimeout(r, 6000));
91
+ } finally {
92
+ renderer?.destroy();
93
+ }
94
+ }
95
+
96
+ await main().catch((err: unknown) => {
97
+ process.stderr.write(`visual-header-demo failed:\n${formatError(err)}\n`);
98
+ process.exit(1);
99
+ });
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ UNAME="$(uname)"
5
+ if [[ "${UNAME}" != "Darwin" ]]; then
6
+ echo "Error: visual regression capture requires macOS." >&2
7
+ exit 2
8
+ fi
9
+
10
+ for cmd in osascript swift screencapture magick python3; do
11
+ if ! command -v "${cmd}" >/dev/null 2>&1; then
12
+ echo "Error: required command not found: ${cmd}" >&2
13
+ exit 2
14
+ fi
15
+ done
16
+
17
+ ITERM_APP_PATH="${LLXPRT_UI_ITERM_APP_PATH:-/Applications/iTerm2.app}"
18
+ if [[ ! -d "${ITERM_APP_PATH}" ]]; then
19
+ echo "Error: iTerm2 is not installed at ${ITERM_APP_PATH}" >&2
20
+ echo "Install iTerm2 or set LLXPRT_UI_ITERM_APP_PATH to the correct path." >&2
21
+ exit 2
22
+ fi
23
+
24
+ ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
25
+
26
+ UPDATE_BASELINE=0
27
+ if [[ "${1:-}" == "--update-baseline" ]]; then
28
+ UPDATE_BASELINE=1
29
+ fi
30
+
31
+ ARTIFACTS_DIR="${ROOT_DIR}/packages/ui/visual-artifacts"
32
+ BASELINES_DIR="${ROOT_DIR}/packages/ui/visual-baselines"
33
+ BASELINE_PATH="${BASELINES_DIR}/header.png"
34
+
35
+ mkdir -p "${ARTIFACTS_DIR}"
36
+ mkdir -p "${BASELINES_DIR}"
37
+
38
+ RAW_PATH="${ARTIFACTS_DIR}/header-raw.png"
39
+ CROP_PATH="${ARTIFACTS_DIR}/header-crop.png"
40
+ DIFF_PATH="${ARTIFACTS_DIR}/header-diff.png"
41
+
42
+ # Window size (in points as used by macOS UI scripting).
43
+ WINDOW_SIZE="${LLXPRT_UI_VISUAL_WINDOW_SIZE:-1400,650}"
44
+ # Crop within the captured window: y,height
45
+ CROP_Y="${LLXPRT_UI_VISUAL_CROP_Y:-80}"
46
+ CROP_H="${LLXPRT_UI_VISUAL_CROP_H:-220}"
47
+ STARTUP_DELAY="${LLXPRT_UI_VISUAL_DELAY_SEC:-2}"
48
+
49
+ IFS=',' read -r WIN_W WIN_H <<< "${WINDOW_SIZE}"
50
+
51
+ if [[ -z "${WIN_W}" || -z "${WIN_H}" ]]; then
52
+ echo "Invalid LLXPRT_UI_VISUAL_WINDOW_SIZE: '${WINDOW_SIZE}'" >&2
53
+ exit 2
54
+ fi
55
+
56
+ COMMAND="cd ${ROOT_DIR} && bun run packages/ui/scripts/visual-header-demo.ts"
57
+
58
+ WINDOW_ID="$(osascript <<APPLESCRIPT
59
+ on run
60
+ set winW to ${WIN_W}
61
+ set winH to ${WIN_H}
62
+ set cmd to "${COMMAND}"
63
+
64
+ tell application "iTerm2"
65
+ activate
66
+ set demoWindow to (create window with default profile)
67
+ tell current session of demoWindow
68
+ write text cmd
69
+ end tell
70
+ set demoId to id of demoWindow
71
+ end tell
72
+
73
+ delay ${STARTUP_DELAY}
74
+
75
+ tell application "System Events"
76
+ tell process "iTerm2"
77
+ set frontmost to true
78
+ set size of front window to {winW, winH}
79
+ end tell
80
+ end tell
81
+
82
+ return demoId
83
+ end run
84
+ APPLESCRIPT
85
+ )"
86
+
87
+ cleanup() {
88
+ if [[ -n "${WINDOW_ID:-}" ]]; then
89
+ osascript -e "tell application \"iTerm2\" to close (first window whose id is ${WINDOW_ID})" >/dev/null 2>&1 || true
90
+ fi
91
+ }
92
+ trap cleanup EXIT
93
+
94
+ if [[ -z "${WINDOW_ID}" ]]; then
95
+ echo "Failed to create iTerm2 window for visual capture." >&2
96
+ exit 2
97
+ fi
98
+
99
+ BOUNDS="$(swift - <<SWIFT
100
+ import Foundation
101
+ import CoreGraphics
102
+
103
+ let target = ${WINDOW_ID}
104
+ let options: CGWindowListOption = [.optionOnScreenOnly, .excludeDesktopElements]
105
+ let infoList = CGWindowListCopyWindowInfo(options, kCGNullWindowID) as? [[String: Any]] ?? []
106
+
107
+ for info in infoList {
108
+ let owner = info[kCGWindowOwnerName as String] as? String ?? ""
109
+ if owner != "iTerm" { continue }
110
+ guard let number = info[kCGWindowNumber as String] as? Int, number == target else { continue }
111
+ guard let bounds = info[kCGWindowBounds as String] as? [String: Any] else { continue }
112
+ let x = bounds["X"] as? Int ?? 0
113
+ let y = bounds["Y"] as? Int ?? 0
114
+ let w = bounds["Width"] as? Int ?? 0
115
+ let h = bounds["Height"] as? Int ?? 0
116
+ print("\\(x),\\(y),\\(w),\\(h)")
117
+ break
118
+ }
119
+ SWIFT
120
+ )"
121
+
122
+ if [[ -z "${BOUNDS}" ]]; then
123
+ echo "Failed to locate iTerm window bounds for window id ${WINDOW_ID}." >&2
124
+ echo "Make sure iTerm2 is running and Accessibility permissions are granted." >&2
125
+ exit 2
126
+ fi
127
+
128
+ IFS=',' read -r WIN_X WIN_Y WIN_W_FOUND WIN_H_FOUND <<< "${BOUNDS}"
129
+ if [[ -z "${WIN_X}" || -z "${WIN_Y}" || -z "${WIN_W_FOUND}" || -z "${WIN_H_FOUND}" ]]; then
130
+ echo "Failed to parse window bounds: '${BOUNDS}'" >&2
131
+ exit 2
132
+ fi
133
+
134
+ # Capture the window region by coordinates (more reliable than -l on some setups).
135
+ screencapture -x -R "${WIN_X},${WIN_Y},${WIN_W_FOUND},${WIN_H_FOUND}" "${RAW_PATH}"
136
+
137
+ magick "${RAW_PATH}" -crop "${WIN_W_FOUND}x${CROP_H}+0+${CROP_Y}" +repage "${CROP_PATH}"
138
+
139
+ # Sanity-check the capture contains the expected green border (avoid saving wallpaper/blank captures).
140
+ GREEN_FRACTION="$(magick "${CROP_PATH}" -alpha off -fuzz 10% -fill white -opaque "#2d7d46" -fill black +opaque "#2d7d46" -colorspace Gray -format "%[fx:mean]" info:)"
141
+ if [[ -z "${GREEN_FRACTION}" ]]; then
142
+ echo "Failed to compute capture sanity check metric" >&2
143
+ exit 2
144
+ fi
145
+ python3 - <<PY
146
+ import sys
147
+ fraction=float("${GREEN_FRACTION}")
148
+ if fraction < 0.0005:
149
+ sys.stderr.write(f"Capture sanity check failed for ${CROP_PATH} (green border fraction={fraction}).\\n")
150
+ sys.stderr.write("This usually means macOS Screen Recording permission is missing for your terminal/shell.\\n")
151
+ sys.exit(2)
152
+ PY
153
+
154
+ if [[ "${UPDATE_BASELINE}" -eq 1 ]]; then
155
+ cp "${CROP_PATH}" "${BASELINE_PATH}"
156
+ echo "Updated baseline: ${BASELINE_PATH}"
157
+ trap - EXIT
158
+ cleanup
159
+ exit 0
160
+ fi
161
+
162
+ if [[ ! -f "${BASELINE_PATH}" ]]; then
163
+ echo "Baseline not found: ${BASELINE_PATH}" >&2
164
+ echo "Run: LLXPRT_UI_VISUAL_WINDOW_SIZE='${WINDOW_SIZE}' $(basename "${BASH_SOURCE[0]}") --update-baseline" >&2
165
+ exit 2
166
+ fi
167
+
168
+ set +e
169
+ METRIC="$(magick compare -metric AE "${BASELINE_PATH}" "${CROP_PATH}" "${DIFF_PATH}" 2>&1)"
170
+ STATUS=$?
171
+ set -e
172
+
173
+ if [[ "${STATUS}" -eq 0 ]]; then
174
+ echo "Visual regression passed"
175
+ trap - EXIT
176
+ cleanup
177
+ exit 0
178
+ fi
179
+
180
+ echo "Visual regression failed: ${METRIC} differing pixels"
181
+ echo "Artifacts:"
182
+ echo " baseline: ${BASELINE_PATH}"
183
+ echo " current : ${CROP_PATH}"
184
+ echo " diff : ${DIFF_PATH}"
185
+ trap - EXIT
186
+ cleanup
187
+ exit 1
@@ -1,27 +1,12 @@
1
1
  import { existsSync } from 'node:fs';
2
- import path from 'node:path';
3
2
  import { fileURLToPath } from 'node:url';
4
3
  import React from 'react';
5
- import { useRenderer } from '@vybestack/opentui-react';
6
- import { useEffect, useState } from 'react';
7
4
  import type { ThemeDefinition } from '../../features/theme';
8
- import { getLogger } from '../../lib/logger';
9
5
 
10
- const logger = getLogger('nui:headerbar');
11
-
12
- // Get the directory of this source file, then navigate to the logo
13
- const __filename = fileURLToPath(import.meta.url);
14
- const __dirname = path.dirname(__filename);
15
- const LOGO_PATH = path.resolve(__dirname, '../../../llxprt.png');
16
-
17
- logger.debug('HeaderBar module loaded', {
18
- __filename,
19
- __dirname,
20
- LOGO_PATH,
21
- logoExists: existsSync(LOGO_PATH),
22
- });
23
- const LOGO_PX_WIDTH = 150;
24
- const LOGO_PX_HEIGHT = 90;
6
+ const DEFAULT_LOGO_PATH = fileURLToPath(
7
+ new URL('../../../llxprt.png', import.meta.url),
8
+ );
9
+ const LOGO_ASPECT_RATIO = 415 / 260;
25
10
 
26
11
  interface HeaderBarProps {
27
12
  readonly text: string;
@@ -29,74 +14,13 @@ interface HeaderBarProps {
29
14
  }
30
15
 
31
16
  export function HeaderBar({ text, theme }: HeaderBarProps): React.ReactNode {
32
- const renderer = useRenderer();
33
-
34
- const caps = renderer.capabilities as {
35
- pixelResolution?: { width: number; height: number };
36
- } | null;
37
- const resolution = caps?.pixelResolution ?? renderer.resolution ?? null;
38
- const cellMetrics = renderer.getCellMetrics() ?? null;
39
-
40
- // Log graphics support and resolution detection
41
- logger.debug('HeaderBar render', {
42
- graphicsSupport: renderer.graphicsSupport,
43
- termProgram: process.env.TERM_PROGRAM,
44
- term: process.env.TERM,
45
- resolution,
46
- cellMetrics,
47
- rendererResolution: renderer.resolution,
48
- terminalWidth: renderer.terminalWidth,
49
- terminalHeight: renderer.terminalHeight,
50
- });
51
- const [, setTick] = useState(0);
52
-
53
- useEffect(() => {
54
- const refresh = () => setTick((t) => t + 1);
55
- renderer.on('capabilities', refresh);
56
- renderer.on('pixelResolution', refresh);
57
- renderer.on('resize', refresh);
58
- return () => {
59
- renderer.off('capabilities', refresh);
60
- renderer.off('pixelResolution', refresh);
61
- renderer.off('resize', refresh);
62
- };
63
- }, [renderer]);
64
-
65
- const pxPerCellX =
66
- resolution && renderer.terminalWidth > 0
67
- ? resolution.width / renderer.terminalWidth
68
- : null;
69
- const pxPerCellY =
70
- resolution && renderer.terminalHeight > 0
71
- ? resolution.height / renderer.terminalHeight
72
- : null;
73
- const desiredCellHeight = 2;
74
- const scaleFactor = 0.9; // modest shrink to keep it inside the border
75
- const fallbackPxPerCellX = 9;
76
- const fallbackPxPerCellY = 20;
77
- const scaledPixelHeight = Math.round(
78
- pxPerCellY != null
79
- ? Math.min(
80
- LOGO_PX_HEIGHT * scaleFactor,
81
- pxPerCellY * desiredCellHeight * scaleFactor,
82
- )
83
- : LOGO_PX_HEIGHT * scaleFactor,
84
- );
85
- const scaledPixelWidth = Math.max(
86
- 1,
87
- Math.round((scaledPixelHeight * LOGO_PX_WIDTH) / LOGO_PX_HEIGHT),
88
- );
89
- const effPxPerCellX = pxPerCellX ?? fallbackPxPerCellX;
90
- const effPxPerCellY = pxPerCellY ?? fallbackPxPerCellY;
91
- const logoWidthCells = Math.max(
92
- 1,
93
- Math.ceil(scaledPixelWidth / effPxPerCellX),
94
- );
95
- const logoHeightCells = Math.max(
96
- 1,
97
- Math.ceil(scaledPixelHeight / effPxPerCellY),
17
+ const headerHeight = 3;
18
+ const themeLogoPath = fileURLToPath(
19
+ new URL(`../../../logos/${theme.slug}.png`, import.meta.url),
98
20
  );
99
- const headerHeight = Math.max(logoHeightCells + 1, 3);
21
+ const logoPath = existsSync(themeLogoPath)
22
+ ? themeLogoPath
23
+ : DEFAULT_LOGO_PATH;
100
24
 
101
25
  return (
102
26
  <box
@@ -113,22 +37,20 @@ export function HeaderBar({ text, theme }: HeaderBarProps): React.ReactNode {
113
37
  backgroundColor: theme.colors.panel.headerBg ?? theme.colors.panel.bg,
114
38
  alignItems: 'center',
115
39
  flexDirection: 'row',
116
- gap: 0,
117
40
  justifyContent: 'flex-start',
118
41
  }}
119
42
  >
120
43
  <image
121
- src={LOGO_PATH}
44
+ src={logoPath}
122
45
  alt="LLxprt Code"
123
- width={logoWidthCells}
124
- height={logoHeightCells}
125
- pixelWidth={scaledPixelWidth}
126
- pixelHeight={scaledPixelHeight}
46
+ height={1}
47
+ aspectRatio={LOGO_ASPECT_RATIO}
48
+ backgroundColor={theme.colors.panel.headerBg ?? theme.colors.panel.bg}
127
49
  style={{ marginRight: 1 }}
128
50
  />
129
51
  <text
130
52
  fg={theme.colors.panel.headerFg ?? theme.colors.text.primary}
131
- style={{ marginLeft: 1, alignSelf: 'center' }}
53
+ style={{ alignSelf: 'center' }}
132
54
  >
133
55
  {text}
134
56
  </text>