From 184b7c4e2f44ad7a99e6b6342e51ddfca6395797 Mon Sep 17 00:00:00 2001 From: NiveditJain Date: Sat, 9 May 2026 17:48:41 -0700 Subject: [PATCH 1/7] chore: trim startup banner to 8 rows so it doesn't look vertically stretched MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Drop the f-top-serif row and the bottom row of the p-descender (rows 1 and 10 of the original 10-row design). Many monospace fonts render each cell ~2-3× taller than wide, so 10 char-rows of pixel-block letters end up reading as a tall rectangle on screen even though the source-pixel ratio is fine. 8 rows keeps the curl/crossbar/foot of "f", a stub descender on "p", and every other letter shape unchanged. Co-Authored-By: Claude Opus 4.7 --- scripts/launch.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/launch.ts b/scripts/launch.ts index d09d897..ec377e1 100644 --- a/scripts/launch.ts +++ b/scripts/launch.ts @@ -17,10 +17,11 @@ export function launch(mode: "dev" | "start"): void { // chunky lowercase "failproof ai" compressed with Unicode 2x2 quadrant // block characters (▖▗▘▙▚▛▜▝▞▟ + ▀ ▄ █ ▌ ▐) and then horizontally // scaled 4:3 (every 4th source-pixel column dropped) so the full - // wordmark fits in ~75 cols × ~10 rows — clean on any standard ≥80-col - // terminal. + // wordmark fits in ~75 cols × ~8 rows — clean on any standard ≥80-col + // terminal. Many monospace fonts render each cell ~2-3× taller than wide, + // so we drop the f-top-serif row and trim the p-descender to a single + // row to keep the wordmark from looking visually stretched vertically. const bannerLines = [ - " ███ ▐███ ▐█", " ▐█▛▀▀ ▟█▖ ▟█▛▀▀ ▝▀", " ██████ ▗████▌ ▝██▛ ██ ███ ▐██▙ ▗███ ▐██▙ ▗█████▙ ████▌ ▐█", " ▀▜█▛▀▀ ▝▀▀▀▀█▙ ▄▙ ██ ▗▟▀▀▜▄▖ ▄▟▀▀▘ ▄▟▛▀▜▙▖ ▄█▀▀█▄▖▝▀██▛▀▀ ▀▀▀▀▙▄ ▐█", @@ -29,7 +30,6 @@ export function launch(mode: "dev" | "start"): void { " ▐█▌ ▝▀█████ ██▄▄██ ▐████▀▘ ██ ▀▜███▀▘ ▀▜███▀▘ ██▌ ▀█████ ▐█", " ▝▀▘ ▀▀▀▀▀ ▀▀▀▀▀▀ ▐█▀▀▀ ▀▀ ▝▀▀▀ ▝▀▀▀ ▀▀▘ ▀▀▀▀▀ ▝▀", " ▐█", - " ▝▀", ]; // Fall back to plain text on narrow terminals so the wide pixel-block art // doesn't wrap and shred itself. process.stdout.columns is undefined when From 5256736aaa0d38ec7dcd28a9f31d4ba9c23f3d8f Mon Sep 17 00:00:00 2001 From: NiveditJain Date: Sat, 9 May 2026 17:49:19 -0700 Subject: [PATCH 2/7] docs: changelog entry for banner trim Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ca1468..1f428f3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## 0.0.10-beta.10 — 2026-05-09 ### Fixes +- `scripts/launch.ts`: trim the dashboard-startup ASCII wordmark from 10 to 8 char-rows (drop the `f`-top-serif row and the bottom row of the `p`-descender) so the chunky pixel-block banner doesn't read as a vertically-stretched rectangle on monospace fonts that render each cell ~2-3× taller than wide. All letters stay recognizable — `f` keeps its curl + crossbar, `p` keeps a stub descender, every other shape unchanged (#338). - Read full session UUID from each Gemini JSONL's metadata header at project-page session-listing time (`lib/gemini-projects.ts`), so links route to a valid `[sessionId]` segment instead of the 8-hex filename prefix that the session detail route's `UUID_RE` check rejects (404). Hooks-section links were already correct because hook stdin carries the full UUID; this aligns the projects-section with that path (#336). - Canonicalize OpenCode and Pi tool-input arg keys so the path-checking builtin policies actually fire on `read` / `write` / `edit` tool calls. OpenCode delivers args as `filePath` / `oldString` / `newString` / `replaceAll`; Pi delivers `path`. The failproofai builtins read `ctx.toolInput.file_path`, so the shape mismatch silently no-op'd `block-read-outside-cwd` (OpenCode), `block-env-files`, and `block-secrets-write` for both CLIs — letting an OpenCode session read paths outside its CWD without any deny, and letting Pi sessions write to `.env` / SSH-key paths unchecked. Note: `block-read-outside-cwd` already worked on Pi via an existing `tool_input.path` fallback at `src/hooks/builtin-policies.ts:796`, so only `block-env-files` and `block-secrets-write` were affected on Pi. Mirrors the `OPENCODE_TOOL_MAP` / `PI_TOOL_MAP` pattern from PR #293 with two new per-tool maps keyed by canonical PascalCase tool name: `OPENCODE_TOOL_INPUT_MAP` (Read / Write / Edit) and `PI_TOOL_INPUT_MAP` (Read / Write / Edit, top-level `path` only — Pi's nested `edits[{oldText,newText}]` array isn't a flat key rename). Both maps are mirrored inline in their respective shims so `.opencode/plugins/failproofai.mjs` and `pi-extension/index.ts` stay self-contained; MCP `mcp_*` and any unmapped tool pass through unchanged. Existing OpenCode users must regenerate their shim via `failproofai policies --install --cli opencode` to pick up the fix; Pi users must reinstall via `failproofai policies --install --cli pi` (#337). - Route OpenCode project pages by encoded cwd (`encodeFolderName(worktree)`) instead of opencode's project name / basename, fixing the dashboard `/project/` 404 for OpenCode-only sessions and merging same-cwd OpenCode + other-CLI rows on the Projects page (#335). From 35b76763f3439eef328e5cdd8276ebc1e698b419 Mon Sep 17 00:00:00 2001 From: NiveditJain Date: Sat, 9 May 2026 17:54:21 -0700 Subject: [PATCH 3/7] docs: bump 0.0.10-beta.10 changelog header to 2026-05-10 (UTC) Per CodeRabbit on PR #338: section was dated 2026-05-09 but the actual UTC date is 2026-05-10, and the project rule for the section heading date is "today's date". Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f428f3..4d50f07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -## 0.0.10-beta.10 — 2026-05-09 +## 0.0.10-beta.10 — 2026-05-10 ### Fixes - `scripts/launch.ts`: trim the dashboard-startup ASCII wordmark from 10 to 8 char-rows (drop the `f`-top-serif row and the bottom row of the `p`-descender) so the chunky pixel-block banner doesn't read as a vertically-stretched rectangle on monospace fonts that render each cell ~2-3× taller than wide. All letters stay recognizable — `f` keeps its curl + crossbar, `p` keeps a stub descender, every other shape unchanged (#338). From a329bfd0e37f7a61c6e170a7036685bc38449742 Mon Sep 17 00:00:00 2001 From: NiveditJain Date: Sat, 9 May 2026 18:15:32 -0700 Subject: [PATCH 4/7] =?UTF-8?q?chore:=20shrink=20banner=20further=20from?= =?UTF-8?q?=208=20=E2=86=92=206=20char-rows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 8-row trim still rendered as roughly square on terminals where each cell is ~2× taller than wide. Drop one more middle-body row so the bowls of a/p/o use only top-arc + bottom-arc (no 3-row interior); f and the descender of p still survive. Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 2 +- scripts/launch.ts | 12 +++++++----- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d50f07..3684e0f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ ## 0.0.10-beta.10 — 2026-05-10 ### Fixes -- `scripts/launch.ts`: trim the dashboard-startup ASCII wordmark from 10 to 8 char-rows (drop the `f`-top-serif row and the bottom row of the `p`-descender) so the chunky pixel-block banner doesn't read as a vertically-stretched rectangle on monospace fonts that render each cell ~2-3× taller than wide. All letters stay recognizable — `f` keeps its curl + crossbar, `p` keeps a stub descender, every other shape unchanged (#338). +- `scripts/launch.ts`: trim the dashboard-startup ASCII wordmark from 10 to 6 char-rows (drop the `f`-top-serif row, a middle-body row, and the bottom row of the `p`-descender) so the chunky pixel-block banner doesn't read as a vertically-stretched rectangle on monospace fonts that render each cell ~2-3× taller than wide. Letters stay readable: `f` keeps its curl + crossbar + stem + foot, the bowls of `a`/`p`/`o` collapse from a 3-row interior to 2 (top-arc + bottom-arc), and `p` keeps a stub descender (#338). - Read full session UUID from each Gemini JSONL's metadata header at project-page session-listing time (`lib/gemini-projects.ts`), so links route to a valid `[sessionId]` segment instead of the 8-hex filename prefix that the session detail route's `UUID_RE` check rejects (404). Hooks-section links were already correct because hook stdin carries the full UUID; this aligns the projects-section with that path (#336). - Canonicalize OpenCode and Pi tool-input arg keys so the path-checking builtin policies actually fire on `read` / `write` / `edit` tool calls. OpenCode delivers args as `filePath` / `oldString` / `newString` / `replaceAll`; Pi delivers `path`. The failproofai builtins read `ctx.toolInput.file_path`, so the shape mismatch silently no-op'd `block-read-outside-cwd` (OpenCode), `block-env-files`, and `block-secrets-write` for both CLIs — letting an OpenCode session read paths outside its CWD without any deny, and letting Pi sessions write to `.env` / SSH-key paths unchecked. Note: `block-read-outside-cwd` already worked on Pi via an existing `tool_input.path` fallback at `src/hooks/builtin-policies.ts:796`, so only `block-env-files` and `block-secrets-write` were affected on Pi. Mirrors the `OPENCODE_TOOL_MAP` / `PI_TOOL_MAP` pattern from PR #293 with two new per-tool maps keyed by canonical PascalCase tool name: `OPENCODE_TOOL_INPUT_MAP` (Read / Write / Edit) and `PI_TOOL_INPUT_MAP` (Read / Write / Edit, top-level `path` only — Pi's nested `edits[{oldText,newText}]` array isn't a flat key rename). Both maps are mirrored inline in their respective shims so `.opencode/plugins/failproofai.mjs` and `pi-extension/index.ts` stay self-contained; MCP `mcp_*` and any unmapped tool pass through unchanged. Existing OpenCode users must regenerate their shim via `failproofai policies --install --cli opencode` to pick up the fix; Pi users must reinstall via `failproofai policies --install --cli pi` (#337). - Route OpenCode project pages by encoded cwd (`encodeFolderName(worktree)`) instead of opencode's project name / basename, fixing the dashboard `/project/` 404 for OpenCode-only sessions and merging same-cwd OpenCode + other-CLI rows on the Projects page (#335). diff --git a/scripts/launch.ts b/scripts/launch.ts index ec377e1..174c058 100644 --- a/scripts/launch.ts +++ b/scripts/launch.ts @@ -17,16 +17,18 @@ export function launch(mode: "dev" | "start"): void { // chunky lowercase "failproof ai" compressed with Unicode 2x2 quadrant // block characters (▖▗▘▙▚▛▜▝▞▟ + ▀ ▄ █ ▌ ▐) and then horizontally // scaled 4:3 (every 4th source-pixel column dropped) so the full - // wordmark fits in ~75 cols × ~8 rows — clean on any standard ≥80-col + // wordmark fits in ~75 cols × ~6 rows — clean on any standard ≥80-col // terminal. Many monospace fonts render each cell ~2-3× taller than wide, - // so we drop the f-top-serif row and trim the p-descender to a single - // row to keep the wordmark from looking visually stretched vertically. + // so we drop both the f-top-serif row AND a middle-body row, AND trim + // the p-descender to a single row, to keep the wordmark from looking + // visually stretched vertically. Letters stay readable: f keeps its + // curl + crossbar + stem + foot, the bowls of a/p/o collapse from a + // 3-row interior to 2 rows (top-arc + bottom-arc), and p keeps a stub + // descender. const bannerLines = [ - " ▐█▛▀▀ ▟█▖ ▟█▛▀▀ ▝▀", " ██████ ▗████▌ ▝██▛ ██ ███ ▐██▙ ▗███ ▐██▙ ▗█████▙ ████▌ ▐█", " ▀▜█▛▀▀ ▝▀▀▀▀█▙ ▄▙ ██ ▗▟▀▀▜▄▖ ▄▟▀▀▘ ▄▟▛▀▜▙▖ ▄█▀▀█▄▖▝▀██▛▀▀ ▀▀▀▀▙▄ ▐█", " ▐█▌ ▗██████ ███ ██ ▐█ ▐█▌ ██ ██▌ ▐█▌ ██ ██▌ ██▌ ██████ ▐█", - " ▐█▌ ▐█▛▀▀██ ██▀ ██ ▐█ ▐█▌ ██ ██▌ ▐█▌ ██ ██▌ ██▌ █▛▀▀██ ▐█", " ▐█▌ ▝▀█████ ██▄▄██ ▐████▀▘ ██ ▀▜███▀▘ ▀▜███▀▘ ██▌ ▀█████ ▐█", " ▝▀▘ ▀▀▀▀▀ ▀▀▀▀▀▀ ▐█▀▀▀ ▀▀ ▝▀▀▀ ▝▀▀▀ ▀▀▘ ▀▀▀▀▀ ▝▀", " ▐█", From 8c369d598e96dd08f414326c6662c696577781ec Mon Sep 17 00:00:00 2001 From: NiveditJain Date: Sat, 9 May 2026 18:41:04 -0700 Subject: [PATCH 5/7] [luv-339] feat: render dashboard banner from actual brand wordmark Replace the hand-crafted monochrome pixel-block banner in scripts/launch.ts with a colored half-block render of the actual failproofai wordmark logo. A new scripts/generate-banner.ts reads assets/wordmark/source.png, crops to the non-transparent bounding box, box-filter downsamples to a 46x7 character grid (= 46x14 source pixels via the U+2580 upper-half-block trick so each char encodes 2 vertically-stacked source pixels at 1:1 aspect), and emits scripts/banner.generated.ts with two pre-rendered arrays: - coloredBanner: 24-bit ANSI fg/bg per cell so the teal flower over the "i" and the pink "l"-stem highlights from the brand mark survive - monoBanner: same grid in plain Unicode block chars (- _ # space) scripts/launch.ts picks the variant: TTY + COLORTERM=truecolor/24bit (or FORCE_COLOR=3 to override the isTTY check chalk-style) selects coloredBanner; NO_COLOR=1, non-TTY without FORCE_COLOR, or non-truecolor terminals fall back to monoBanner; and the existing plain-text "failproof ai" fallback still kicks in when the terminal is narrower than the art. The PNG itself is not loaded at runtime - only by scripts/generate-banner.ts when re-generating the bundled banner. pngjs + @types/pngjs added as devDeps for the generator. New unit tests in __tests__/scripts/banner.test.ts pin the grid dimensions, ANSI presence in the colored variant, mono-only-blocks shape, visible-pixel count, and per-line cell width matches BANNER_COLS. Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 3 + __tests__/scripts/banner.test.ts | 44 +++++++++ assets/wordmark/source.png | Bin 0 -> 17983 bytes bun.lock | 6 ++ package.json | 4 +- scripts/banner.generated.ts | 26 ++++++ scripts/generate-banner.ts | 151 +++++++++++++++++++++++++++++++ scripts/launch.ts | 62 +++++++------ 8 files changed, 266 insertions(+), 30 deletions(-) create mode 100644 __tests__/scripts/banner.test.ts create mode 100644 assets/wordmark/source.png create mode 100644 scripts/banner.generated.ts create mode 100644 scripts/generate-banner.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3684e0f..7687add 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,9 @@ ## 0.0.10-beta.10 — 2026-05-10 +### Features +- `scripts/launch.ts`: replace the hand-crafted monochrome pixel-block dashboard banner with a colored half-block render of the actual brand wordmark (committed as `assets/wordmark/source.png`). New `scripts/generate-banner.ts` reads the PNG, crops to its non-transparent bounding box, box-filter downsamples to a 46×7 character grid (= 46×14 source pixels via the `▀` upper-half-block trick — each cell encodes 2 vertically-stacked source pixels at 1:1 aspect), and emits two pre-rendered arrays into `scripts/banner.generated.ts`: `coloredBanner` (24-bit ANSI fg/bg per cell so the teal flower over the `i` and the pink `l`-stem highlights survive) and `monoBanner` (the same grid in plain Unicode block chars). Launch now picks `coloredBanner` when stdout is a TTY with `COLORTERM=truecolor`/`24bit` or `FORCE_COLOR=3`, `monoBanner` for `NO_COLOR=1` / non-TTY / non-truecolor terminals, and the existing plain-text `failproof ai` fallback when the terminal is narrower than the art (#339). + ### Fixes - `scripts/launch.ts`: trim the dashboard-startup ASCII wordmark from 10 to 6 char-rows (drop the `f`-top-serif row, a middle-body row, and the bottom row of the `p`-descender) so the chunky pixel-block banner doesn't read as a vertically-stretched rectangle on monospace fonts that render each cell ~2-3× taller than wide. Letters stay readable: `f` keeps its curl + crossbar + stem + foot, the bowls of `a`/`p`/`o` collapse from a 3-row interior to 2 (top-arc + bottom-arc), and `p` keeps a stub descender (#338). - Read full session UUID from each Gemini JSONL's metadata header at project-page session-listing time (`lib/gemini-projects.ts`), so links route to a valid `[sessionId]` segment instead of the 8-hex filename prefix that the session detail route's `UUID_RE` check rejects (404). Hooks-section links were already correct because hook stdin carries the full UUID; this aligns the projects-section with that path (#336). diff --git a/__tests__/scripts/banner.test.ts b/__tests__/scripts/banner.test.ts new file mode 100644 index 0000000..fb31a88 --- /dev/null +++ b/__tests__/scripts/banner.test.ts @@ -0,0 +1,44 @@ +// @vitest-environment node +import { describe, it, expect } from "vitest"; +import { coloredBanner, monoBanner, BANNER_COLS, BANNER_ROWS } from "@/scripts/banner.generated"; + +describe("banner.generated", () => { + it("exports the configured grid dimensions", () => { + expect(BANNER_COLS).toBeGreaterThan(0); + expect(BANNER_ROWS).toBeGreaterThan(0); + }); + + it("colored and mono banners both have BANNER_ROWS lines", () => { + expect(coloredBanner).toHaveLength(BANNER_ROWS); + expect(monoBanner).toHaveLength(BANNER_ROWS); + }); + + it("mono banner uses only block chars + spaces (no ANSI escapes)", () => { + for (const line of monoBanner) { + expect(line).not.toMatch(/\x1b/); + expect(line).toMatch(/^[ ▀▄█]*$/); + } + }); + + it("mono banner includes pixel content (not all whitespace)", () => { + const totalPixels = monoBanner.reduce( + (n, line) => n + (line.match(/[▀▄█]/gu)?.length ?? 0), + 0, + ); + expect(totalPixels).toBeGreaterThan(20); + }); + + it("colored banner contains 24-bit ANSI escapes", () => { + const joined = coloredBanner.join(""); + expect(joined).toMatch(/\x1b\[38;2;\d+;\d+;\d+m/); + expect(joined).toMatch(/\x1b\[0m/); + }); + + it("colored banner cell count per line matches BANNER_COLS", () => { + const ansiRe = /\x1b\[[0-9;]*m/g; + for (const line of coloredBanner) { + const visible = line.replace(ansiRe, ""); + expect([...visible].length).toBe(BANNER_COLS); + } + }); +}); diff --git a/assets/wordmark/source.png b/assets/wordmark/source.png new file mode 100644 index 0000000000000000000000000000000000000000..3df9a574372a01a2eed68e1ca0a2d0dffb706f12 GIT binary patch literal 17983 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 ALPHA_THRESHOLD) { + if (x < bbLeft) bbLeft = x; + if (x > bbRight) bbRight = x; + if (y < bbTop) bbTop = y; + if (y > bbBottom) bbBottom = y; + } + } +} +if (bbRight < 0) throw new Error("source PNG appears fully transparent"); +const cropW = bbRight - bbLeft + 1; +const cropH = bbBottom - bbTop + 1; + +// Box-filter downsample: each target pixel = average RGBA over its source +// rectangle. Alpha-weighted RGB averaging avoids halo from transparent +// pixels bleeding into edge cells. +const dstW = TARGET_COLS; +const dstH = TARGET_ROWS * 2; +type Px = { r: number; g: number; b: number; a: number }; +const dst: Px[] = new Array(dstW * dstH); +for (let dy = 0; dy < dstH; dy++) { + const y0 = bbTop + Math.floor((dy * cropH) / dstH); + const y1 = bbTop + Math.floor(((dy + 1) * cropH) / dstH); + for (let dx = 0; dx < dstW; dx++) { + const x0 = bbLeft + Math.floor((dx * cropW) / dstW); + const x1 = bbLeft + Math.floor(((dx + 1) * cropW) / dstW); + let rSum = 0, gSum = 0, bSum = 0, aSum = 0, weightSum = 0, count = 0; + for (let y = y0; y < y1; y++) { + for (let x = x0; x < x1; x++) { + const i = (y * srcW + x) * 4; + const a = data[i + 3]; + rSum += data[i] * a; + gSum += data[i + 1] * a; + bSum += data[i + 2] * a; + aSum += a; + weightSum += a; + count++; + } + } + const avgA = count ? Math.round(aSum / count) : 0; + const px: Px = weightSum > 0 + ? { + r: Math.round(rSum / weightSum), + g: Math.round(gSum / weightSum), + b: Math.round(bSum / weightSum), + a: avgA, + } + : { r: 0, g: 0, b: 0, a: 0 }; + dst[dy * dstW + dx] = px; + } +} + +const ESC = "\x1b["; +const RESET = `${ESC}0m`; +const fg = (p: Px) => `${ESC}38;2;${p.r};${p.g};${p.b}m`; +const bg = (p: Px) => `${ESC}48;2;${p.r};${p.g};${p.b}m`; +const bgDefault = `${ESC}49m`; + +const coloredLines: string[] = []; +const monoLines: string[] = []; +for (let row = 0; row < TARGET_ROWS; row++) { + let colored = ""; + let mono = ""; + for (let col = 0; col < TARGET_COLS; col++) { + const top = dst[(row * 2) * dstW + col]; + const bot = dst[(row * 2 + 1) * dstW + col]; + const topOn = top.a >= ALPHA_THRESHOLD; + const botOn = bot.a >= ALPHA_THRESHOLD; + if (!topOn && !botOn) { + colored += `${RESET} `; + mono += " "; + } else if (topOn && !botOn) { + colored += `${fg(top)}${bgDefault}▀`; + mono += "▀"; + } else if (!topOn && botOn) { + colored += `${fg(bot)}${bgDefault}▄`; + mono += "▄"; + } else { + colored += `${fg(top)}${bg(bot)}▀`; + mono += "█"; + } + } + coloredLines.push(colored + RESET); + monoLines.push(mono.replace(/\s+$/, "")); +} + +const header = `// Auto-generated by scripts/generate-banner.ts. Do not edit by hand. +// Source: assets/wordmark/source.png +// Grid: ${TARGET_COLS} cols × ${TARGET_ROWS} rows (= ${TARGET_COLS}×${TARGET_ROWS * 2} source pixels via ▀ half-block). +`; +const fmt = (lines: string[]) => + "[\n" + lines.map(l => " " + JSON.stringify(l)).join(",\n") + ",\n]"; + +writeFileSync( + outPath, + `${header} +export const BANNER_COLS = ${TARGET_COLS}; +export const BANNER_ROWS = ${TARGET_ROWS}; + +export const coloredBanner: string[] = ${fmt(coloredLines)}; + +export const monoBanner: string[] = ${fmt(monoLines)}; +`, +); + +console.log(`wrote ${outPath} (${TARGET_COLS}×${TARGET_ROWS}, source bbox ${cropW}×${cropH})`); diff --git a/scripts/launch.ts b/scripts/launch.ts index 174c058..86587c6 100644 --- a/scripts/launch.ts +++ b/scripts/launch.ts @@ -8,40 +8,44 @@ import { fileURLToPath } from "node:url"; import { parseScriptArgs } from "./parse-script-args"; import { diagnoseShadow } from "./install-diagnosis.mjs"; import { version } from "../package.json"; +import { coloredBanner, monoBanner, BANNER_COLS } from "./banner.generated"; export function launch(mode: "dev" | "start"): void { const { loggingLevel, disableTelemetry, allowedDevOrigins, remainingArgs } = parseScriptArgs(process.argv.slice(2)); - // Hand-crafted pixel-block wordmark mirroring the hosted PNG logo at - // https://d2wq11aau0arks.cloudfront.net/failproof/logo-wordmark.png — - // chunky lowercase "failproof ai" compressed with Unicode 2x2 quadrant - // block characters (▖▗▘▙▚▛▜▝▞▟ + ▀ ▄ █ ▌ ▐) and then horizontally - // scaled 4:3 (every 4th source-pixel column dropped) so the full - // wordmark fits in ~75 cols × ~6 rows — clean on any standard ≥80-col - // terminal. Many monospace fonts render each cell ~2-3× taller than wide, - // so we drop both the f-top-serif row AND a middle-body row, AND trim - // the p-descender to a single row, to keep the wordmark from looking - // visually stretched vertically. Letters stay readable: f keeps its - // curl + crossbar + stem + foot, the bowls of a/p/o collapse from a - // 3-row interior to 2 rows (top-arc + bottom-arc), and p keeps a stub - // descender. - const bannerLines = [ - " ██████ ▗████▌ ▝██▛ ██ ███ ▐██▙ ▗███ ▐██▙ ▗█████▙ ████▌ ▐█", - " ▀▜█▛▀▀ ▝▀▀▀▀█▙ ▄▙ ██ ▗▟▀▀▜▄▖ ▄▟▀▀▘ ▄▟▛▀▜▙▖ ▄█▀▀█▄▖▝▀██▛▀▀ ▀▀▀▀▙▄ ▐█", - " ▐█▌ ▗██████ ███ ██ ▐█ ▐█▌ ██ ██▌ ▐█▌ ██ ██▌ ██▌ ██████ ▐█", - " ▐█▌ ▝▀█████ ██▄▄██ ▐████▀▘ ██ ▀▜███▀▘ ▀▜███▀▘ ██▌ ▀█████ ▐█", - " ▝▀▘ ▀▀▀▀▀ ▀▀▀▀▀▀ ▐█▀▀▀ ▀▀ ▝▀▀▀ ▝▀▀▀ ▀▀▘ ▀▀▀▀▀ ▝▀", - " ▐█", - ]; - // Fall back to plain text on narrow terminals so the wide pixel-block art - // doesn't wrap and shred itself. process.stdout.columns is undefined when - // stdout isn't a TTY (piped, captured, redirected to a file), in which case - // there's no width to compare against and we keep the full art as-is. - const bannerWidth = bannerLines.reduce((w, l) => Math.max(w, l.length), 0); + // Pre-rendered wordmark — see scripts/generate-banner.ts. Two variants of + // the same logo: coloredBanner uses 24-bit ANSI fg/bg per cell with `▀` + // half-blocks (each char encodes 2 vertically-stacked source pixels at + // 1:1 aspect, so the teal flower + pink "l" highlights survive); monoBanner + // is the same grid in plain Unicode block chars for terminals where 24-bit + // color is unavailable (NO_COLOR, non-TTY, basic terminals). const cols = process.stdout.columns; - const banner = cols !== undefined && cols < bannerWidth - ? " failproof ai" - : bannerLines.join("\n"); + const fitsArt = cols === undefined || cols >= BANNER_COLS + 2; + // FORCE_COLOR=3 follows the chalk/supports-color convention: explicitly + // forces 24-bit color even when stdout isn't a TTY (useful for piping + // colored output into a pager or capturing for sharing). + const forceTruecolor = process.env.FORCE_COLOR === "3"; + const noColor = + process.env.NO_COLOR !== undefined + || process.env.FORCE_COLOR === "0" + || (!process.stdout.isTTY && !forceTruecolor); + // Most modern terminals (iTerm2, kitty, alacritty, Windows Terminal, + // gnome-terminal, vscode, wezterm) advertise 24-bit support via + // COLORTERM=truecolor. When COLORTERM isn't set we'd rather emit clean + // monochrome blocks than risk garbled escapes on a basic terminal. + const supports24bit = + !noColor + && (process.env.COLORTERM === "truecolor" + || process.env.COLORTERM === "24bit" + || forceTruecolor); + let banner: string; + if (!fitsArt) { + banner = " failproof ai"; + } else if (supports24bit) { + banner = " " + coloredBanner.join("\n "); + } else { + banner = " " + monoBanner.join("\n "); + } console.log(`\n${banner}\n\n v${version}\n`); console.log(` ⭐ Star us: https://github.com/exospherehost/failproofai`); console.log(` 📖 Docs: https://befailproof.ai`); From 69515a07227ad93a642c08a45d8f03d64834d8de Mon Sep 17 00:00:00 2001 From: NiveditJain Date: Sat, 9 May 2026 18:49:48 -0700 Subject: [PATCH 6/7] [luv-339] test: pin exact banner grid dimensions Tighten the dimension assertion in __tests__/scripts/banner.test.ts from `> 0` to the concrete `BANNER_COLS = 46` / `BANNER_ROWS = 7` values so an accidental regeneration at a different footprint trips this test instead of silently shipping a resized startup banner. Addresses CodeRabbit review comment on PR #338. Co-Authored-By: Claude Opus 4.7 --- __tests__/scripts/banner.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/__tests__/scripts/banner.test.ts b/__tests__/scripts/banner.test.ts index fb31a88..7d41079 100644 --- a/__tests__/scripts/banner.test.ts +++ b/__tests__/scripts/banner.test.ts @@ -4,8 +4,11 @@ import { coloredBanner, monoBanner, BANNER_COLS, BANNER_ROWS } from "@/scripts/b describe("banner.generated", () => { it("exports the configured grid dimensions", () => { - expect(BANNER_COLS).toBeGreaterThan(0); - expect(BANNER_ROWS).toBeGreaterThan(0); + // Pin the concrete footprint so an accidental regeneration at a + // different size trips this test instead of silently shipping a + // resized startup banner. + expect(BANNER_COLS).toBe(46); + expect(BANNER_ROWS).toBe(7); }); it("colored and mono banners both have BANNER_ROWS lines", () => { From 91ff0f5066e2317ed75f92d46593e05023bffeb9 Mon Sep 17 00:00:00 2001 From: NiveditJain Date: Sat, 9 May 2026 18:57:07 -0700 Subject: [PATCH 7/7] [luv-339] fix: drop startup ASCII wordmark; align version with link lines Every iteration of the dashboard-startup banner this PR tried (the original 10-row pixel-block, the 6-row trim, and the colored half-block render of the brand PNG) read poorly in standard terminals - too tall, vertically stretched, or just visual noise. Drop the banner entirely. scripts/launch.ts now prints a plain-text "failproof ai" title and a "Version" line padded to the same column as the existing "Star us" / "Docs" / "Slack" lines, so version and URLs form one cleanly-aligned block: failproof ai Version: 0.0.10-beta.10 Star us: https://github.com/exospherehost/failproofai Docs: https://befailproof.ai Slack: https://join.slack.com/... Removes the now-unused pipeline: - scripts/generate-banner.ts (PNG -> half-block generator) - scripts/banner.generated.ts (pre-rendered banner data) - assets/wordmark/source.png (brand wordmark source) - __tests__/scripts/banner.test.ts - pngjs + @types/pngjs devDeps CHANGELOG entry under 0.0.10-beta.10 -> Fixes consolidates the prior "trim from 10 to 6 rows" and the "colored half-block render" entries from earlier commits on this branch into a single Fixes line. Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 5 +- __tests__/scripts/banner.test.ts | 47 ---------- assets/wordmark/source.png | Bin 17983 -> 0 bytes bun.lock | 6 -- package.json | 2 - scripts/banner.generated.ts | 26 ------ scripts/generate-banner.ts | 151 ------------------------------- scripts/launch.ts | 40 +------- 8 files changed, 6 insertions(+), 271 deletions(-) delete mode 100644 __tests__/scripts/banner.test.ts delete mode 100644 assets/wordmark/source.png delete mode 100644 scripts/banner.generated.ts delete mode 100644 scripts/generate-banner.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 7687add..7c4389d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,11 +2,8 @@ ## 0.0.10-beta.10 — 2026-05-10 -### Features -- `scripts/launch.ts`: replace the hand-crafted monochrome pixel-block dashboard banner with a colored half-block render of the actual brand wordmark (committed as `assets/wordmark/source.png`). New `scripts/generate-banner.ts` reads the PNG, crops to its non-transparent bounding box, box-filter downsamples to a 46×7 character grid (= 46×14 source pixels via the `▀` upper-half-block trick — each cell encodes 2 vertically-stacked source pixels at 1:1 aspect), and emits two pre-rendered arrays into `scripts/banner.generated.ts`: `coloredBanner` (24-bit ANSI fg/bg per cell so the teal flower over the `i` and the pink `l`-stem highlights survive) and `monoBanner` (the same grid in plain Unicode block chars). Launch now picks `coloredBanner` when stdout is a TTY with `COLORTERM=truecolor`/`24bit` or `FORCE_COLOR=3`, `monoBanner` for `NO_COLOR=1` / non-TTY / non-truecolor terminals, and the existing plain-text `failproof ai` fallback when the terminal is narrower than the art (#339). - ### Fixes -- `scripts/launch.ts`: trim the dashboard-startup ASCII wordmark from 10 to 6 char-rows (drop the `f`-top-serif row, a middle-body row, and the bottom row of the `p`-descender) so the chunky pixel-block banner doesn't read as a vertically-stretched rectangle on monospace fonts that render each cell ~2-3× taller than wide. Letters stay readable: `f` keeps its curl + crossbar + stem + foot, the bowls of `a`/`p`/`o` collapse from a 3-row interior to 2 (top-arc + bottom-arc), and `p` keeps a stub descender (#338). +- `scripts/launch.ts`: drop the dashboard-startup ASCII wordmark entirely. Every iteration (the original 10-row pixel-block banner, the 6-row trim, and the colored half-block render of the brand PNG) read poorly in standard terminals — too tall, vertically stretched, or just visual noise. Replace with a plain-text `failproof ai` title and a `📦 Version: ` line padded to the same column as the existing `⭐ Star us:` / `📖 Docs:` / `💬 Slack:` lines, so version and URLs form one cleanly-aligned block. Removes `scripts/generate-banner.ts`, `scripts/banner.generated.ts`, `assets/wordmark/source.png`, the `pngjs` + `@types/pngjs` devDeps, and `__tests__/scripts/banner.test.ts` that were added earlier in this branch's history (#338). - Read full session UUID from each Gemini JSONL's metadata header at project-page session-listing time (`lib/gemini-projects.ts`), so links route to a valid `[sessionId]` segment instead of the 8-hex filename prefix that the session detail route's `UUID_RE` check rejects (404). Hooks-section links were already correct because hook stdin carries the full UUID; this aligns the projects-section with that path (#336). - Canonicalize OpenCode and Pi tool-input arg keys so the path-checking builtin policies actually fire on `read` / `write` / `edit` tool calls. OpenCode delivers args as `filePath` / `oldString` / `newString` / `replaceAll`; Pi delivers `path`. The failproofai builtins read `ctx.toolInput.file_path`, so the shape mismatch silently no-op'd `block-read-outside-cwd` (OpenCode), `block-env-files`, and `block-secrets-write` for both CLIs — letting an OpenCode session read paths outside its CWD without any deny, and letting Pi sessions write to `.env` / SSH-key paths unchecked. Note: `block-read-outside-cwd` already worked on Pi via an existing `tool_input.path` fallback at `src/hooks/builtin-policies.ts:796`, so only `block-env-files` and `block-secrets-write` were affected on Pi. Mirrors the `OPENCODE_TOOL_MAP` / `PI_TOOL_MAP` pattern from PR #293 with two new per-tool maps keyed by canonical PascalCase tool name: `OPENCODE_TOOL_INPUT_MAP` (Read / Write / Edit) and `PI_TOOL_INPUT_MAP` (Read / Write / Edit, top-level `path` only — Pi's nested `edits[{oldText,newText}]` array isn't a flat key rename). Both maps are mirrored inline in their respective shims so `.opencode/plugins/failproofai.mjs` and `pi-extension/index.ts` stay self-contained; MCP `mcp_*` and any unmapped tool pass through unchanged. Existing OpenCode users must regenerate their shim via `failproofai policies --install --cli opencode` to pick up the fix; Pi users must reinstall via `failproofai policies --install --cli pi` (#337). - Route OpenCode project pages by encoded cwd (`encodeFolderName(worktree)`) instead of opencode's project name / basename, fixing the dashboard `/project/` 404 for OpenCode-only sessions and merging same-cwd OpenCode + other-CLI rows on the Projects page (#335). diff --git a/__tests__/scripts/banner.test.ts b/__tests__/scripts/banner.test.ts deleted file mode 100644 index 7d41079..0000000 --- a/__tests__/scripts/banner.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -// @vitest-environment node -import { describe, it, expect } from "vitest"; -import { coloredBanner, monoBanner, BANNER_COLS, BANNER_ROWS } from "@/scripts/banner.generated"; - -describe("banner.generated", () => { - it("exports the configured grid dimensions", () => { - // Pin the concrete footprint so an accidental regeneration at a - // different size trips this test instead of silently shipping a - // resized startup banner. - expect(BANNER_COLS).toBe(46); - expect(BANNER_ROWS).toBe(7); - }); - - it("colored and mono banners both have BANNER_ROWS lines", () => { - expect(coloredBanner).toHaveLength(BANNER_ROWS); - expect(monoBanner).toHaveLength(BANNER_ROWS); - }); - - it("mono banner uses only block chars + spaces (no ANSI escapes)", () => { - for (const line of monoBanner) { - expect(line).not.toMatch(/\x1b/); - expect(line).toMatch(/^[ ▀▄█]*$/); - } - }); - - it("mono banner includes pixel content (not all whitespace)", () => { - const totalPixels = monoBanner.reduce( - (n, line) => n + (line.match(/[▀▄█]/gu)?.length ?? 0), - 0, - ); - expect(totalPixels).toBeGreaterThan(20); - }); - - it("colored banner contains 24-bit ANSI escapes", () => { - const joined = coloredBanner.join(""); - expect(joined).toMatch(/\x1b\[38;2;\d+;\d+;\d+m/); - expect(joined).toMatch(/\x1b\[0m/); - }); - - it("colored banner cell count per line matches BANNER_COLS", () => { - const ansiRe = /\x1b\[[0-9;]*m/g; - for (const line of coloredBanner) { - const visible = line.replace(ansiRe, ""); - expect([...visible].length).toBe(BANNER_COLS); - } - }); -}); diff --git a/assets/wordmark/source.png b/assets/wordmark/source.png deleted file mode 100644 index 3df9a574372a01a2eed68e1ca0a2d0dffb706f12..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 17983 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 ALPHA_THRESHOLD) { - if (x < bbLeft) bbLeft = x; - if (x > bbRight) bbRight = x; - if (y < bbTop) bbTop = y; - if (y > bbBottom) bbBottom = y; - } - } -} -if (bbRight < 0) throw new Error("source PNG appears fully transparent"); -const cropW = bbRight - bbLeft + 1; -const cropH = bbBottom - bbTop + 1; - -// Box-filter downsample: each target pixel = average RGBA over its source -// rectangle. Alpha-weighted RGB averaging avoids halo from transparent -// pixels bleeding into edge cells. -const dstW = TARGET_COLS; -const dstH = TARGET_ROWS * 2; -type Px = { r: number; g: number; b: number; a: number }; -const dst: Px[] = new Array(dstW * dstH); -for (let dy = 0; dy < dstH; dy++) { - const y0 = bbTop + Math.floor((dy * cropH) / dstH); - const y1 = bbTop + Math.floor(((dy + 1) * cropH) / dstH); - for (let dx = 0; dx < dstW; dx++) { - const x0 = bbLeft + Math.floor((dx * cropW) / dstW); - const x1 = bbLeft + Math.floor(((dx + 1) * cropW) / dstW); - let rSum = 0, gSum = 0, bSum = 0, aSum = 0, weightSum = 0, count = 0; - for (let y = y0; y < y1; y++) { - for (let x = x0; x < x1; x++) { - const i = (y * srcW + x) * 4; - const a = data[i + 3]; - rSum += data[i] * a; - gSum += data[i + 1] * a; - bSum += data[i + 2] * a; - aSum += a; - weightSum += a; - count++; - } - } - const avgA = count ? Math.round(aSum / count) : 0; - const px: Px = weightSum > 0 - ? { - r: Math.round(rSum / weightSum), - g: Math.round(gSum / weightSum), - b: Math.round(bSum / weightSum), - a: avgA, - } - : { r: 0, g: 0, b: 0, a: 0 }; - dst[dy * dstW + dx] = px; - } -} - -const ESC = "\x1b["; -const RESET = `${ESC}0m`; -const fg = (p: Px) => `${ESC}38;2;${p.r};${p.g};${p.b}m`; -const bg = (p: Px) => `${ESC}48;2;${p.r};${p.g};${p.b}m`; -const bgDefault = `${ESC}49m`; - -const coloredLines: string[] = []; -const monoLines: string[] = []; -for (let row = 0; row < TARGET_ROWS; row++) { - let colored = ""; - let mono = ""; - for (let col = 0; col < TARGET_COLS; col++) { - const top = dst[(row * 2) * dstW + col]; - const bot = dst[(row * 2 + 1) * dstW + col]; - const topOn = top.a >= ALPHA_THRESHOLD; - const botOn = bot.a >= ALPHA_THRESHOLD; - if (!topOn && !botOn) { - colored += `${RESET} `; - mono += " "; - } else if (topOn && !botOn) { - colored += `${fg(top)}${bgDefault}▀`; - mono += "▀"; - } else if (!topOn && botOn) { - colored += `${fg(bot)}${bgDefault}▄`; - mono += "▄"; - } else { - colored += `${fg(top)}${bg(bot)}▀`; - mono += "█"; - } - } - coloredLines.push(colored + RESET); - monoLines.push(mono.replace(/\s+$/, "")); -} - -const header = `// Auto-generated by scripts/generate-banner.ts. Do not edit by hand. -// Source: assets/wordmark/source.png -// Grid: ${TARGET_COLS} cols × ${TARGET_ROWS} rows (= ${TARGET_COLS}×${TARGET_ROWS * 2} source pixels via ▀ half-block). -`; -const fmt = (lines: string[]) => - "[\n" + lines.map(l => " " + JSON.stringify(l)).join(",\n") + ",\n]"; - -writeFileSync( - outPath, - `${header} -export const BANNER_COLS = ${TARGET_COLS}; -export const BANNER_ROWS = ${TARGET_ROWS}; - -export const coloredBanner: string[] = ${fmt(coloredLines)}; - -export const monoBanner: string[] = ${fmt(monoLines)}; -`, -); - -console.log(`wrote ${outPath} (${TARGET_COLS}×${TARGET_ROWS}, source bbox ${cropW}×${cropH})`); diff --git a/scripts/launch.ts b/scripts/launch.ts index 86587c6..afe082d 100644 --- a/scripts/launch.ts +++ b/scripts/launch.ts @@ -8,45 +8,15 @@ import { fileURLToPath } from "node:url"; import { parseScriptArgs } from "./parse-script-args"; import { diagnoseShadow } from "./install-diagnosis.mjs"; import { version } from "../package.json"; -import { coloredBanner, monoBanner, BANNER_COLS } from "./banner.generated"; export function launch(mode: "dev" | "start"): void { const { loggingLevel, disableTelemetry, allowedDevOrigins, remainingArgs } = parseScriptArgs(process.argv.slice(2)); - // Pre-rendered wordmark — see scripts/generate-banner.ts. Two variants of - // the same logo: coloredBanner uses 24-bit ANSI fg/bg per cell with `▀` - // half-blocks (each char encodes 2 vertically-stacked source pixels at - // 1:1 aspect, so the teal flower + pink "l" highlights survive); monoBanner - // is the same grid in plain Unicode block chars for terminals where 24-bit - // color is unavailable (NO_COLOR, non-TTY, basic terminals). - const cols = process.stdout.columns; - const fitsArt = cols === undefined || cols >= BANNER_COLS + 2; - // FORCE_COLOR=3 follows the chalk/supports-color convention: explicitly - // forces 24-bit color even when stdout isn't a TTY (useful for piping - // colored output into a pager or capturing for sharing). - const forceTruecolor = process.env.FORCE_COLOR === "3"; - const noColor = - process.env.NO_COLOR !== undefined - || process.env.FORCE_COLOR === "0" - || (!process.stdout.isTTY && !forceTruecolor); - // Most modern terminals (iTerm2, kitty, alacritty, Windows Terminal, - // gnome-terminal, vscode, wezterm) advertise 24-bit support via - // COLORTERM=truecolor. When COLORTERM isn't set we'd rather emit clean - // monochrome blocks than risk garbled escapes on a basic terminal. - const supports24bit = - !noColor - && (process.env.COLORTERM === "truecolor" - || process.env.COLORTERM === "24bit" - || forceTruecolor); - let banner: string; - if (!fitsArt) { - banner = " failproof ai"; - } else if (supports24bit) { - banner = " " + coloredBanner.join("\n "); - } else { - banner = " " + monoBanner.join("\n "); - } - console.log(`\n${banner}\n\n v${version}\n`); + // Plain-text title + a labeled `Version` line that lines up with the + // `Star us` / `Docs` / `Slack` lines below (all four labels pad to the + // same column so the values form a clean right-hand column). + console.log(`\n failproof ai\n`); + console.log(` 📦 Version: ${version}`); console.log(` ⭐ Star us: https://github.com/exospherehost/failproofai`); console.log(` 📖 Docs: https://befailproof.ai`); console.log(` 💬 Slack: https://join.slack.com/t/failproofai/shared_invite/zt-3v63b7k5e-O3NBHmj8X6n9gZSGDx6ggQ\n`);