diff --git a/README.md b/README.md
index 60939b1..f9126cf 100644
--- a/README.md
+++ b/README.md
@@ -18,6 +18,13 @@
Daily coding-agent work means juggling repos, branches, and sessions, and the current tools are chat-first. pidex is the kitchen bench: every active repo, its branch, its sessions, and what's stale, visible in one glance.
+## In action
+
+| | |
+|---|---|
+|  |  |
+| App booting, workspaces populate | The workbench, fully loaded |
+
## Quick Start
You need [mise](https://mise.jdx.dev/) and the [Pi CLI](https://pi.dev/). The dashboard reads existing sessions from `~/.pi/agent/sessions`.
@@ -82,10 +89,17 @@ The workbench inherits its palette from the [Pi coding agent](https://pi.dev) so
Regenerate assets after a token edit:
```bash
-uv run --with fonttools python scripts/build-icon.py # assets/icon, mark
-uv run --with fonttools python scripts/build-screenshot.py # docs/assets/screenshot
+uv run --with fonttools python scripts/build-icon.py # assets/icon, mark
+uv run --with fonttools python scripts/build-logo.py # assets/logo (composable π + pidex)
+uv run --with fonttools python scripts/build-banner.py # docs/assets/banner-bg
```
+`docs/assets/screenshot.png` is captured from the running app. To refresh: temporarily gate a `webContents.capturePage()` call behind an env var, run `PIDEX_CAPTURE=1 mise run dev`, then revert. The banner composites the screenshot over the design-system background (`docs/assets/banner-bg.png`, built by `scripts/build-banner.py`) with the composable logo watermark, all via `magick`.
+
+The banner background is procedural: `bg-canvas` field with the blueprint grid (96px minor + 480px major, lifted in opacity for hero scale) and a vertical gradient into `bg-deep`. Strictly rectilinear; no generated imagery.
+
+Banner and logo use the design system font pairing: **Georgia** (serif, from `--serif` in `tokens.css`) for the wordmark and body, **Commit Mono** (from `--mono`) for the code surfaces visible in the screenshot.
+
## What's next
- [ ] Diff viewer with virtualised summaries
diff --git a/assets/icon-premium.jpg b/assets/icon-premium.jpg
deleted file mode 100644
index 3e1d497..0000000
Binary files a/assets/icon-premium.jpg and /dev/null differ
diff --git a/assets/icon.png b/assets/icon.png
deleted file mode 100644
index 1bc6600..0000000
Binary files a/assets/icon.png and /dev/null differ
diff --git a/assets/icon.svg b/assets/icon.svg
deleted file mode 100644
index f86a3dc..0000000
--- a/assets/icon.svg
+++ /dev/null
@@ -1,12 +0,0 @@
-
diff --git a/assets/logo.png b/assets/logo.png
new file mode 100644
index 0000000..cd49968
Binary files /dev/null and b/assets/logo.png differ
diff --git a/assets/logo.svg b/assets/logo.svg
new file mode 100644
index 0000000..f5bdd89
--- /dev/null
+++ b/assets/logo.svg
@@ -0,0 +1,18 @@
+
diff --git a/assets/mark.png b/assets/mark.png
deleted file mode 100644
index 2c5b599..0000000
Binary files a/assets/mark.png and /dev/null differ
diff --git a/assets/mark.svg b/assets/mark.svg
deleted file mode 100644
index 7101342..0000000
--- a/assets/mark.svg
+++ /dev/null
@@ -1,5 +0,0 @@
-
diff --git a/docs/assets/banner-bg.png b/docs/assets/banner-bg.png
new file mode 100644
index 0000000..951b364
Binary files /dev/null and b/docs/assets/banner-bg.png differ
diff --git a/docs/assets/banner-bg.svg b/docs/assets/banner-bg.svg
new file mode 100644
index 0000000..20eb204
--- /dev/null
+++ b/docs/assets/banner-bg.svg
@@ -0,0 +1,24 @@
+
diff --git a/docs/assets/banner.png b/docs/assets/banner.png
index c0a3fe6..8592c8b 100644
Binary files a/docs/assets/banner.png and b/docs/assets/banner.png differ
diff --git a/docs/assets/framed-window.svg b/docs/assets/framed-window.svg
new file mode 100644
index 0000000..886a5a8
--- /dev/null
+++ b/docs/assets/framed-window.svg
@@ -0,0 +1,34 @@
+
\ No newline at end of file
diff --git a/docs/assets/screenshot.png b/docs/assets/screenshot.png
index 7dbb4a4..b6519b5 100644
Binary files a/docs/assets/screenshot.png and b/docs/assets/screenshot.png differ
diff --git a/docs/assets/screenshot.svg b/docs/assets/screenshot.svg
deleted file mode 100644
index e84180d..0000000
--- a/docs/assets/screenshot.svg
+++ /dev/null
@@ -1,59 +0,0 @@
-
diff --git a/docs/assets/workbench-boot.gif b/docs/assets/workbench-boot.gif
new file mode 100644
index 0000000..fa540da
Binary files /dev/null and b/docs/assets/workbench-boot.gif differ
diff --git a/docs/assets/workbench-loaded.gif b/docs/assets/workbench-loaded.gif
new file mode 100644
index 0000000..a8d0499
Binary files /dev/null and b/docs/assets/workbench-loaded.gif differ
diff --git a/package.json b/package.json
index ba988ef..b868406 100644
--- a/package.json
+++ b/package.json
@@ -9,7 +9,7 @@
"dev": "mise run dev",
"build": "mise run build",
"start": "mise run start",
- "lint": "oxlint src tests",
+ "lint": "bunx oxlint src tests",
"typecheck": "bunx tsc -p tsconfig.json --noEmit && bunx tsc -p tsconfig.main.json --noEmit && bunx tsc -p tsconfig.preload.json --noEmit",
"test": "mise run test",
"test:unit": "bunx vitest run --project unit",
diff --git a/scripts/_design.py b/scripts/_design.py
index fe0b2cc..68ee3ec 100644
--- a/scripts/_design.py
+++ b/scripts/_design.py
@@ -8,18 +8,16 @@
Token groups:
- Warm darks (window bg, panel surfaces)
- Creams (text, marks, lit facet)
- - Terracotta / sunkissed (warm accents, the fluid-crest peak)
+ - Terracotta / sunkissed (warm accents)
- Sage / accent-blue (secondary accents)
"""
from __future__ import annotations
-from fontTools.ttLib import TTFont
-from fontTools.pens.svgPathPen import SVGPathPen
-
# --- Warm darks ---
# pi.dev: --color-warm-black
WARM_BLACK = "#13110f"
+INK_BRONZE = WARM_BLACK
# pi.dev: --panel-base
PANEL = "#212730"
# pi.dev: --panel-soft-base
@@ -56,18 +54,4 @@
# pi.dev: --text
TEXT = "#ebe7e4"
# pi.dev: --muted
-MUTED = "#9fa4ab"
-
-DEFAULT_FONT = "/System/Library/Fonts/Supplemental/Georgia Italic.ttf"
-
-
-def pi_path(font_path: str = DEFAULT_FONT) -> tuple[str, tuple[int, int, int, int], int]:
- """Return (svg_path_d, bbox, unitsPerEm) for the π glyph (U+03C0)."""
- font = TTFont(font_path)
- cmap = font.getBestCmap()
- gname = cmap[ord("π")]
- pen = SVGPathPen(font.getGlyphSet())
- font.getGlyphSet()[gname].draw(pen)
- glyf = font["glyf"][gname]
- bbox = (glyf.xMin, glyf.yMin, glyf.xMax, glyf.yMax)
- return pen.getCommands(), bbox, font["head"].unitsPerEm
\ No newline at end of file
+MUTED = "#9fa4ab"
\ No newline at end of file
diff --git a/scripts/build-banner.py b/scripts/build-banner.py
new file mode 100644
index 0000000..60fee2a
--- /dev/null
+++ b/scripts/build-banner.py
@@ -0,0 +1,86 @@
+"""Build the README banner background using the pidex design system.
+
+The background mirrors the app's own chrome (tokens.css base.css body
+background): bg-canvas field with the blueprint grid (96px minor +
+480px major, blue-tinted) and a vertical gradient into bg-deep. Shapes
+are strictly rectilinear; no organic imagery. The real app screenshot
+and the composable logo are laid on top of this background.
+
+Output:
+ docs/assets/banner-bg.png (1600x900)
+
+Compose the final banner with:
+ magick docs/assets/banner-bg.png \\
+ docs/assets/screenshot.png -resize 1000x -geometry +300+80 -composite \\
+ assets/logo.png -resize 200x -geometry +70+70 -composite \\
+ docs/assets/banner.png
+"""
+
+from __future__ import annotations
+
+import shutil
+import subprocess
+import sys
+from pathlib import Path
+
+REPO = Path(__file__).resolve().parent.parent
+OUT = REPO / "docs" / "assets"
+
+sys.path.insert(0, str(Path(__file__).resolve().parent))
+from _design import BG_DEEP, BG_CANVAS # noqa: E402
+
+W, H = 1600, 900
+
+
+def svg() -> str:
+ # Blueprint grid matches tokens.css (96px minor + 480px major, blue
+ # tint). At hero scale the token opacities would be invisible, so
+ # they're lifted slightly while keeping the same hue.
+ return f"""
+"""
+
+
+def render_png(svg: Path, png: Path, width: int) -> None:
+ if not shutil.which("inkscape"):
+ sys.exit("inkscape not found on PATH; install via brew install inkscape")
+ subprocess.run(
+ ["inkscape", str(svg), "--export-type=png", "--export-filename", str(png), "--export-width", str(width)],
+ check=True,
+ )
+
+
+def main() -> None:
+ OUT.mkdir(parents=True, exist_ok=True)
+ svg_path = OUT / "banner-bg.svg"
+ png_path = OUT / "banner-bg.png"
+ svg_path.write_text(svg())
+ render_png(svg_path, png_path, W)
+ print(f"wrote {svg_path}")
+ print(f"wrote {png_path}")
+
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/scripts/build-icon.py b/scripts/build-icon.py
deleted file mode 100644
index 6b02b89..0000000
--- a/scripts/build-icon.py
+++ /dev/null
@@ -1,109 +0,0 @@
-"""Build the pidex app icon and mark SVGs from Georgia Italic, using the
-Pi (pi.dev) design tokens so the workbench visually aligns with the Pi
-runtime.
-
-Outputs:
- assets/icon.svg - flat 2D app icon (warm-black rounded square + cream faceted pi)
- assets/mark.svg - flat pi mark on transparent, for in-UI use
- assets/icon.png - rasterized icon at 1024x1024 (for Electron build/icon.png)
- assets/mark.png - rasterized mark at 1024x1024
-"""
-
-from __future__ import annotations
-
-import shutil
-import subprocess
-import sys
-from pathlib import Path
-
-REPO = Path(__file__).resolve().parent.parent
-ASSETS = REPO / "assets"
-SCRIPTS = Path(__file__).resolve().parent
-
-sys.path.insert(0, str(SCRIPTS))
-from _design import ( # noqa: E402
- WARM_BLACK,
- PARCHMENT,
- MOONSTONE,
- pi_path,
-)
-
-# Icon geometry (1024x1024 canvas, macOS-style squircle radius ~22.37%).
-ICON_SIZE = 1024
-ICON_RADIUS = 229
-
-# π placement: glyph centered, height ~56% of icon.
-SCALE = 0.56
-
-# Screen upper-left triangle (defined in glyph coords; the group transform flips Y).
-LIT_CLIP = "-22,980 1295,980 -22,-19"
-
-
-def build_icon_svg(d: str, bbox: tuple[int, int, int, int]) -> str:
- gx = (bbox[0] + bbox[2]) / 2
- gy = (bbox[1] + bbox[3]) / 2
- cx = cy = ICON_SIZE / 2
- return f"""
-"""
-
-
-def build_mark_svg(d: str, bbox: tuple[int, int, int, int]) -> str:
- gx = (bbox[0] + bbox[2]) / 2
- gy = (bbox[1] + bbox[3]) / 2
- size = ICON_SIZE
- cx = cy = size / 2
- return f"""
-"""
-
-
-def render_png(svg: Path, png: Path, width: int) -> None:
- if not shutil.which("inkscape"):
- sys.exit("inkscape not found on PATH; install via brew install inkscape")
- subprocess.run(
- [
- "inkscape",
- str(svg),
- "--export-type=png",
- "--export-filename",
- str(png),
- "--export-width",
- str(width),
- ],
- check=True,
- )
-
-
-def main() -> None:
- ASSETS.mkdir(exist_ok=True)
- d, bbox, _ = pi_path()
-
- icon_svg = ASSETS / "icon.svg"
- mark_svg = ASSETS / "mark.svg"
- icon_svg.write_text(build_icon_svg(d, bbox))
- mark_svg.write_text(build_mark_svg(d, bbox))
- print(f"wrote {icon_svg}")
- print(f"wrote {mark_svg}")
-
- render_png(icon_svg, ASSETS / "icon.png", ICON_SIZE)
- render_png(mark_svg, ASSETS / "mark.png", ICON_SIZE)
- print(f"wrote {ASSETS / 'icon.png'}")
- print(f"wrote {ASSETS / 'mark.png'}")
-
-
-if __name__ == "__main__":
- main()
\ No newline at end of file
diff --git a/scripts/build-logo.py b/scripts/build-logo.py
new file mode 100644
index 0000000..0091355
--- /dev/null
+++ b/scripts/build-logo.py
@@ -0,0 +1,123 @@
+"""Build the pidex logo as a composable horizontal lockup SVG.
+
+The mark is an isometric cube in the design system's cool/neutral
+surfaces (bg-canvas, panel, bg-deep) with the Pi coding agent's logo
+(P + i dot) painted on its top face, and the "pidex" wordmark to
+the right.
+
+Outputs:
+ assets/logo.svg - horizontal lockup, transparent (composable)
+ assets/logo.png - rasterized lockup at 800x400 for quick embedding
+"""
+
+from __future__ import annotations
+
+import shutil
+import subprocess
+import sys
+from pathlib import Path
+
+REPO = Path(__file__).resolve().parent.parent
+ASSETS = REPO / "assets"
+SCRIPTS = Path(__file__).resolve().parent
+
+sys.path.insert(0, str(SCRIPTS))
+from _design import ( # noqa: E402
+ INK_BRONZE,
+ BG_DEEP,
+ BG_CANVAS,
+ PANEL,
+ PARCHMENT,
+)
+
+# Logo canvas
+W, H = 240, 120
+
+# Isometric cube (edge 48), top face centered around (60, 40)
+E = 48
+DX = E * 0.866 # 41.57
+DY = E * 0.5 # 24
+CX, CY = 60, 40
+TOP_X, TOP_Y = CX, CY - DY # (60, 16)
+RIGHT_X, RIGHT_Y = CX + DX, CY # (101.57, 40)
+BOT_X, BOT_Y = CX, CY + DY # (60, 64)
+LEFT_X, LEFT_Y = CX - DX, CY # (18.43, 40)
+
+# Pi coding agent logo paths (from pi.dev/pi-logo.svg, 800x800 viewBox).
+# Content bbox: x,y ∈ [165.29, 634.72] (470x470 region inside the 800x800).
+P_D = (
+ "M165.29 165.29 H517.36 V400 H400 V517.36 H282.65 V634.72 H165.29 Z "
+ "M282.65 282.65 V400 H400 V282.65 Z"
+)
+I_DOT_D = "M517.36 400 H634.72 V634.72 H517.36 Z"
+
+# Affine transform mapping the Pi logo content (165..635, 165..635) onto
+# the top-face rhombus. The rhombus is a parallelogram with half-width DX
+# and half-height DY (measured from the top-center). The content is a
+# 470x470 square. The affine matrix has a = DX/470, b = DY/470,
+# c = -a, d = b, and translates so the content center maps to the rhombus
+# top-center.
+A = DX / 470.0
+B = DY / 470.0
+PI_TRANSFORM = f"matrix({A} {B} {-A} {B} 60 0)"
+
+# Wordmark
+TEXT = "pidex"
+TEXT_X = 120
+TEXT_Y = 76
+TEXT_SIZE = 32
+TEXT_FONT = "Georgia, 'Plantin MT Pro', 'Plantin MT Std', serif"
+TEXT_WEIGHT = 500
+
+
+def build_logo_svg() -> str:
+ return f"""
+"""
+
+
+def render_png(svg: Path, png: Path, width: int) -> None:
+ if not shutil.which("inkscape"):
+ sys.exit("inkscape not found on PATH; install via brew install inkscape")
+ subprocess.run(
+ [
+ "inkscape",
+ str(svg),
+ "--export-type=png",
+ "--export-filename",
+ str(png),
+ "--export-width",
+ str(width),
+ ],
+ check=True,
+ )
+
+
+def main() -> None:
+ ASSETS.mkdir(exist_ok=True)
+ svg_path = ASSETS / "logo.svg"
+ png_path = ASSETS / "logo.png"
+ svg_path.write_text(build_logo_svg())
+ render_png(svg_path, png_path, 800)
+ print(f"wrote {svg_path}")
+ print(f"wrote {png_path}")
+
+
+if __name__ == "__main__":
+ main()
\ No newline at end of file
diff --git a/scripts/build-screenshot.py b/scripts/build-screenshot.py
deleted file mode 100644
index b651480..0000000
--- a/scripts/build-screenshot.py
+++ /dev/null
@@ -1,213 +0,0 @@
-"""Build a faithful pidex dashboard mockup SVG (and PNG) using the actual
-app design tokens (src/renderer/design-system/tokens.css), which mirrors
-pi.dev. Layout mirrors what the renderer ships: dark window with the
-blueprint grid, panel-toned workspace sidebar on the left, session pane
-on the right, input pill at the bottom. Apple-style soft drop shadow.
-
-Output:
- docs/assets/screenshot.svg
- docs/assets/screenshot.png (1440x900)
-
-This is a structural render rather than a live capture: it uses the real
-tokens (--bg-deep, --panel, --parchment, --terracotta-light) and the real
-blueprint grid pattern so the visual language matches the running app.
-"""
-
-from __future__ import annotations
-
-import shutil
-import subprocess
-import sys
-from pathlib import Path
-
-REPO = Path(__file__).resolve().parent.parent
-OUT = REPO / "docs" / "assets"
-
-sys.path.insert(0, str(Path(__file__).resolve().parent))
-from _design import ( # noqa: E402
- BG_DEEP,
- BG_CANVAS,
- PANEL,
- PANEL_SOFT,
- PARCHMENT,
- TERRACOTTA_LIGHT,
- pi_path,
-)
-
-# Canvas
-W, H = 1440, 900
-
-# Window geometry (centered)
-WW, WH = 1200, 760
-WX = (W - WW) // 2
-WY = (H - WH) // 2
-WIN_R = 14
-
-# Sidebar
-SIDE_W = 300
-SIDE_BG = PANEL
-SIDE_PAD = 22
-
-# Warm neutral ground that complements the dark window.
-# Set to None to render on a transparent canvas (useful when the PNG will be
-# composited over a backdrop, e.g. the README banner).
-BG = None
-
-# Blueprint grid colors (from tokens.css --grid-minor / --grid-major)
-GRID_MINOR = "rgba(255,255,255,0.025)"
-GRID_MAJOR = "rgba(255,255,255,0.06)"
-
-
-def svg() -> str:
- d, bbox, _ = pi_path()
- gx = (bbox[0] + bbox[2]) / 2
- gy = (bbox[1] + bbox[3]) / 2
-
- mark_cx = WX + 36
- mark_cy = WY + 44
- mark_scale = 0.036
- mark_transform = (
- f"translate({mark_cx} {mark_cy}) "
- f"scale({mark_scale} -{mark_scale}) "
- f"translate({-gx} {-gy})"
- )
-
- # Workspace cards
- cards = []
- card_y = WY + 100
- for i in range(4):
- cy = card_y + i * 72
- cards.append(
- f''
- f''
- f''
- f''
- f''
- )
-
- pane_x = WX + SIDE_W
- pane_w = WW - SIDE_W
- header_y = WY + 22
- cards_y = WY + 110
- card_h = 132
-
- header = (
- f''
- f''
- f''
- f''
- f''
- f''
- )
-
- main_cards = []
- for i in range(2):
- cy = cards_y + i * (card_h + 18)
- main_cards.append(
- f''
- f''
- f''
- f''
- f''
- f''
- f''
- )
-
- pill_y = WY + WH - 64
- pill = (
- f''
- f''
- f''
- f''
- f''
- )
-
- # Blueprint grid pattern (matches tokens.css body background)
- # Clipped to the main pane area (not sidebar), so the grid is the
- # distinctive app-chrome look in the session view.
- grid_id = "blueprint"
- grid_def = f"""
-
-
-
-
-
-
-
-
-
- """
-
- return f"""
-"""
-
-
-def render_png(svg: Path, png: Path, width: int) -> None:
- if not shutil.which("inkscape"):
- sys.exit("inkscape not found on PATH; install via brew install inkscape")
- subprocess.run(
- ["inkscape", str(svg), "--export-type=png", "--export-filename", str(png), "--export-width", str(width)],
- check=True,
- )
-
-
-def main() -> None:
- OUT.mkdir(parents=True, exist_ok=True)
- svg_path = OUT / "screenshot.svg"
- png_path = OUT / "screenshot.png"
- svg_path.write_text(svg())
- render_png(svg_path, png_path, W)
- print(f"wrote {svg_path}")
- print(f"wrote {png_path}")
-
-
-if __name__ == "__main__":
- main()
\ No newline at end of file
diff --git a/src/main/sessions.ts b/src/main/sessions.ts
index 4a09660..e5e2257 100644
--- a/src/main/sessions.ts
+++ b/src/main/sessions.ts
@@ -3,7 +3,9 @@ import fs from 'fs';
import os from 'os';
import path from 'path';
-const SESSIONS_ROOT = path.join(os.homedir(), '.pi', 'agent', 'sessions');
+const SESSIONS_ROOT = process.env.PIDEX_SESSIONS_ROOT
+ ? path.resolve(process.env.PIDEX_SESSIONS_ROOT)
+ : path.join(os.homedir(), '.pi', 'agent', 'sessions');
type WorkspaceInfo = {
dirName: string;