Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,12 @@
# Changelog

## 0.0.10-beta.10 — 2026-05-09
## 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).
- 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/<slug>` 404 for OpenCode-only sessions and merging same-cwd OpenCode + other-CLI rows on the Projects page (#335).
Expand Down
47 changes: 47 additions & 0 deletions __tests__/scripts/banner.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// @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);
}
});
});
Binary file added assets/wordmark/source.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,12 +71,14 @@
"access": "public"
},
"devDependencies": {
"@anthropic-ai/sdk": "^0.93.0",
"@tailwindcss/postcss": "^4.1.18",
"@tanstack/react-virtual": "^3.13.18",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/node": "^25.5.2",
"@types/pngjs": "^6.0.5",
"@types/react": "^19.2.13",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
Expand All @@ -86,12 +88,12 @@
"happy-dom": "^20.7.0",
"lucide-react": "^1.0.1",
"next": "^16.2.2",
"pngjs": "^7.0.0",
"react": "^19.2.4",
"react-dom": "^19.2.4",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18",
"typescript": "^6.0.2",
"@anthropic-ai/sdk": "^0.93.0",
"vitest": "^4.0.18"
},
"dependencies": {
Expand Down
26 changes: 26 additions & 0 deletions scripts/banner.generated.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// Auto-generated by scripts/generate-banner.ts. Do not edit by hand.
// Source: assets/wordmark/source.png
// Grid: 46 cols × 7 rows (= 46×14 source pixels via ▀ half-block).

export const BANNER_COLS = 46;
export const BANNER_ROWS = 7;

export const coloredBanner: string[] = [
"\u001b[0m \u001b[38;2;216;214;210m\u001b[49m▄\u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[38;2;216;214;210m\u001b[49m▀\u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[0m",
"\u001b[38;2;216;214;210m\u001b[49m▄\u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[38;2;216;214;210m\u001b[49m▄\u001b[38;2;216;214;210m\u001b[49m▄\u001b[0m \u001b[38;2;216;214;210m\u001b[49m▄\u001b[38;2;216;214;210m\u001b[49m▄\u001b[38;2;216;214;210m\u001b[49m▄\u001b[0m \u001b[38;2;102;209;181m\u001b[48;2;102;209;181m▀\u001b[38;2;102;209;181m\u001b[48;2;102;209;181m▀\u001b[0m \u001b[38;2;228;88;124m\u001b[49m▄\u001b[0m \u001b[0m \u001b[38;2;216;214;210m\u001b[49m▄\u001b[38;2;216;214;210m\u001b[49m▄\u001b[0m \u001b[0m \u001b[0m \u001b[38;2;216;214;210m\u001b[49m▄\u001b[38;2;216;214;210m\u001b[49m▄\u001b[0m \u001b[0m \u001b[38;2;216;214;210m\u001b[49m▄\u001b[38;2;216;214;210m\u001b[49m▄\u001b[0m \u001b[0m \u001b[0m \u001b[38;2;216;214;210m\u001b[49m▄\u001b[38;2;216;214;210m\u001b[49m▄\u001b[0m \u001b[0m \u001b[38;2;216;214;210m\u001b[49m▄\u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[38;2;216;214;210m\u001b[49m▄\u001b[38;2;216;214;210m\u001b[49m▄\u001b[0m \u001b[0m \u001b[0m \u001b[38;2;216;214;210m\u001b[49m▄\u001b[38;2;216;214;210m\u001b[49m▄\u001b[38;2;216;214;210m\u001b[49m▄\u001b[0m \u001b[0m \u001b[38;2;216;214;210m\u001b[49m▄\u001b[0m",
"\u001b[38;2;216;214;210m\u001b[49m▀\u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[38;2;216;214;210m\u001b[49m▀\u001b[38;2;216;214;210m\u001b[49m▀\u001b[0m \u001b[38;2;216;214;210m\u001b[49m▀\u001b[38;2;216;214;210m\u001b[49m▀\u001b[38;2;216;214;210m\u001b[49m▀\u001b[38;2;216;214;210m\u001b[49m▄\u001b[0m \u001b[38;2;228;88;124m\u001b[49m▄\u001b[0m \u001b[38;2;228;88;124m\u001b[48;2;228;88;124m▀\u001b[0m \u001b[38;2;216;214;210m\u001b[49m▄\u001b[38;2;216;214;210m\u001b[49m▀\u001b[38;2;216;214;210m\u001b[49m▀\u001b[38;2;216;214;210m\u001b[49m▄\u001b[0m \u001b[38;2;216;214;210m\u001b[49m▄\u001b[38;2;216;214;210m\u001b[49m▀\u001b[38;2;216;214;210m\u001b[49m▀\u001b[0m \u001b[38;2;216;214;210m\u001b[49m▄\u001b[38;2;216;214;210m\u001b[49m▀\u001b[38;2;216;214;210m\u001b[49m▀\u001b[38;2;216;214;210m\u001b[49m▄\u001b[0m \u001b[38;2;216;214;210m\u001b[49m▄\u001b[38;2;216;214;210m\u001b[49m▀\u001b[38;2;216;214;210m\u001b[49m▀\u001b[38;2;216;214;210m\u001b[49m▄\u001b[0m \u001b[38;2;216;214;210m\u001b[49m▀\u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[38;2;216;214;210m\u001b[49m▀\u001b[38;2;216;214;210m\u001b[49m▀\u001b[0m \u001b[0m \u001b[0m \u001b[38;2;216;214;210m\u001b[49m▀\u001b[38;2;216;214;210m\u001b[49m▀\u001b[38;2;216;214;210m\u001b[49m▀\u001b[38;2;216;214;210m\u001b[49m▄\u001b[0m \u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[0m",
"\u001b[0m \u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[0m \u001b[0m \u001b[0m \u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[38;2;228;88;124m\u001b[48;2;228;88;124m▀\u001b[38;2;228;88;124m\u001b[48;2;228;88;124m▀\u001b[0m \u001b[38;2;228;88;124m\u001b[48;2;228;88;124m▀\u001b[0m \u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[0m \u001b[0m \u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[0m \u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[0m \u001b[0m \u001b[0m \u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[0m \u001b[0m \u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[0m \u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[0m \u001b[0m \u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[0m \u001b[0m \u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[0m \u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[0m",
"\u001b[0m \u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[0m \u001b[0m \u001b[0m \u001b[38;2;216;214;210m\u001b[49m▀\u001b[38;2;216;214;210m\u001b[49m▄\u001b[38;2;216;214;210m\u001b[49m▄\u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[0m \u001b[38;2;228;88;124m\u001b[48;2;228;88;124m▀\u001b[38;2;228;88;124m\u001b[49m▄\u001b[38;2;228;88;124m\u001b[48;2;228;88;124m▀\u001b[0m \u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[38;2;216;214;210m\u001b[49m▄\u001b[38;2;216;214;210m\u001b[49m▄\u001b[38;2;216;214;210m\u001b[49m▀\u001b[0m \u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[0m \u001b[0m \u001b[0m \u001b[38;2;216;214;210m\u001b[49m▀\u001b[38;2;216;214;210m\u001b[49m▄\u001b[38;2;216;214;210m\u001b[49m▄\u001b[38;2;216;214;210m\u001b[49m▀\u001b[0m \u001b[38;2;216;214;210m\u001b[49m▀\u001b[38;2;216;214;210m\u001b[49m▄\u001b[38;2;216;214;210m\u001b[49m▄\u001b[38;2;216;214;210m\u001b[49m▀\u001b[0m \u001b[0m \u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[38;2;216;214;210m\u001b[49m▀\u001b[38;2;216;214;210m\u001b[49m▄\u001b[38;2;216;214;210m\u001b[49m▄\u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[0m \u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[0m",
"\u001b[0m \u001b[38;2;216;214;210m\u001b[49m▀\u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[38;2;216;214;210m\u001b[49m▀\u001b[38;2;216;214;210m\u001b[49m▀\u001b[38;2;216;214;210m\u001b[49m▀\u001b[0m \u001b[38;2;228;88;124m\u001b[49m▀\u001b[38;2;228;88;124m\u001b[49m▀\u001b[38;2;228;88;124m\u001b[49m▀\u001b[0m \u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[38;2;216;214;210m\u001b[49m▀\u001b[38;2;216;214;210m\u001b[49m▀\u001b[0m \u001b[0m \u001b[38;2;216;214;210m\u001b[49m▀\u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[38;2;216;214;210m\u001b[49m▀\u001b[38;2;216;214;210m\u001b[49m▀\u001b[0m \u001b[0m \u001b[0m \u001b[38;2;216;214;210m\u001b[49m▀\u001b[38;2;216;214;210m\u001b[49m▀\u001b[0m \u001b[0m \u001b[0m \u001b[38;2;216;214;210m\u001b[49m▀\u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[38;2;216;214;210m\u001b[49m▀\u001b[38;2;216;214;210m\u001b[49m▀\u001b[38;2;216;214;210m\u001b[49m▀\u001b[0m \u001b[38;2;216;214;210m\u001b[49m▀\u001b[0m",
"\u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[38;2;216;214;210m\u001b[48;2;216;214;210m▀\u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m \u001b[0m",
];

export const monoBanner: string[] = [
" ▄█▀ ██ █",
"▄█▄▄ ▄▄▄ ██ ▄ ▄▄ ▄▄ ▄▄ ▄▄ ▄█▄▄ ▄▄▄ ▄",
"▀█▀▀ ▀▀▀▄ ▄ █ ▄▀▀▄ ▄▀▀ ▄▀▀▄ ▄▀▀▄ ▀█▀▀ ▀▀▀▄ █",
" █ ██████ █ █ █ █ █ █ █ █ █ ████ █",
" █ ▀▄▄█ █▄█ █▄▄▀ █ ▀▄▄▀ ▀▄▄▀ █ ▀▄▄█ █",
" ▀ ▀▀▀ ▀▀▀ █▀▀ ▀ ▀▀ ▀▀ ▀ ▀▀▀ ▀",
" █",
];
Loading