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 + +| | | +|---|---| +| ![Boot](docs/assets/workbench-boot.gif) | ![Loaded](docs/assets/workbench-loaded.gif) | +| 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 @@ + + + + + + + + + + + + + + + + + pidex + 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 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - pidex - - - - - - - - - - - - - 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""" + + + + + + + + + + + + + + + + pidex + +""" + + +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""" - - - - - - - - - - - - - {grid_def} - - - {f'' if BG else ''} - - - - - - - - - - - - - - - - - - - pidex - - - - - - - - - {''.join(cards)} - {header} - {''.join(main_cards)} - {pill} - -""" - - -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;