Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@ node_modules/
dist/
.vite/
.DS_Store
*.log
*.log
__pycache__/
41 changes: 37 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div align="center">

<img src="docs/assets/banner.png" alt="OpenPi, a desktop workbench for the Pi coding agent" width="100%">
<img src="docs/assets/banner.png" alt="pidex, a desktop workbench for the Pi coding agent" width="100%">

**A desktop workbench for the Pi coding agent.**
**Workspace-first, not chat-first.**
Expand All @@ -16,19 +16,25 @@

## Why this exists

Daily coding-agent work means juggling repos, branches, and sessions, and the current tools are chat-first: the primary screen is a conversation, not your work. OpenPi flips that. It is a kitchen bench where every active repo, its branch, its sessions, and what has gone stale are visible at a glance.
Daily coding-agent work means juggling repos, branches, and sessions, and the current tools are chat-first: the primary screen is a conversation, not your work. pidex flips that. It is a kitchen bench where every active repo, its branch, its sessions, and what has gone stale are visible at a glance.

| The pain, observed daily | OpenPi's answer |
| The pain, observed daily | pidex's answer |
|---|---|
| Workspace switching takes too many clicks | One dashboard of every repo with Pi sessions, sorted by last activity |
| Losing the thread when resuming | Repo, branch, and session state as first-class UI, not buried chat history |
| Long sessions get sluggish | [Pretext](https://github.com/chenglou/pretext) measures multiline text off the DOM hot path, ~600x faster than browser measurement |
| Worktrees feel unreliable | Git and worktree state surfaced directly, no chatting with the agent to switch branches |

OpenPi is a personal project: learn by building, sharpen day-to-day DX, and share a cleaner local-first harness pattern for coding agents. Not a commercial product, not an OpenCode clone. Install, BYOK, and provider wiring belong to its sibling, Weldable; OpenPi focuses entirely on the daily coding workflow.
pidex is a personal project: learn by building, sharpen day-to-day DX, and share a cleaner local-first harness pattern for coding agents. Not a commercial product, not an OpenCode clone. Install, BYOK, and provider wiring belong to its sibling, Weldable; pidex focuses entirely on the daily coding workflow.

> A developer's kitchen bench, not a chat pane.

<div align="center">

<img src="docs/assets/screenshot.png" alt="pidex dashboard preview" width="900">

</div>

## 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`, so run `pi` in a repo or two first if you want it to light up.
Expand Down Expand Up @@ -128,6 +134,33 @@ Long term:
- [ ] Task tracker awareness alongside sessions
- [ ] Optional adapters for other agent runtimes

## Design tokens

The workbench inherits its palette from the [Pi coding agent](https://pi.dev) so the desktop harness feels like part of the same family as the runtime it drives. Source of truth: [`scripts/_design.py`](scripts/_design.py), hexes pulled from `pi.dev/style.css`.

| Token | Hex | Role |
|---|---|---|
| warm-black | `#13110f` | Primary window background |
| bg-deep | `#0d1116` | Deepest surface |
| bg-canvas | `#161d27` | Canvas surface |
| panel | `#212730` | Sidebar / panel surface |
| panel-soft | `#252f3d` | Card surface |
| parchment | `#dacbc2` | Named cream, primary mark and text |
| moonstone | `#ebe7e4` | Lighter cream, lit facet |
| driftwood | `#5c5752` | Warm mid-gray, shadow facet |
| terracotta | `#844f3b` | Warm bronze accent |
| terracotta-light | `#b86b52` | Accent highlight, status dots |
| sunkissed | `#e1b06e` | Warm gold accent |
| sage | `#a3a473` | Sage accent |
| accent-blue | `#6a9fcc` | Links, focus rings |

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
```

## Documentation

| Doc | What it covers |
Expand Down
Binary file added assets/icon-premium.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 12 additions & 0 deletions assets/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/mark.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 5 additions & 0 deletions assets/mark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/assets/banner.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
42 changes: 42 additions & 0 deletions docs/assets/screenshot.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
73 changes: 73 additions & 0 deletions scripts/_design.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""Design tokens for pidex, sourced from the Pi coding agent website
(pi.dev/style.css) so the workbench visually aligns with the Pi runtime
it drives.

Provenance: https://pi.dev/style.css, lines ~85-130 (the `:root` token
block). Update these hexes if pi.dev revs its palette.

Token groups:
- Warm darks (window bg, panel surfaces)
- Creams (text, marks, lit facet)
- Terracotta / sunkissed (warm accents, the fluid-crest peak)
- 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"
# pi.dev: --panel-base
PANEL = "#212730"
# pi.dev: --panel-soft-base
PANEL_SOFT = "#252f3d"
# pi.dev: --bg-deep, --bg-canvas
BG_DEEP = "#0d1116"
BG_CANVAS = "#161d27"

# --- Creams ---
# pi.dev: --color-parchment (the named "cream")
PARCHMENT = "#dacbc2"
# pi.dev: --color-moonstone (lighter cream / near-white)
MOONSTONE = "#ebe7e4"
# pi.dev: --color-driftwood (warm mid-gray, shadow tone)
DRIFTWOOD = "#5c5752"

# --- Warm accents (the fluid bronze / terracotta aesthetic) ---
# pi.dev: --color-terracotta
TERRACOTTA = "#844f3b"
# pi.dev: --color-terracotta-light
TERRACOTTA_LIGHT = "#b86b52"
# pi.dev: --color-sunkissed
SUNKISSED = "#e1b06e"

# --- Secondary accents ---
# pi.dev: --color-sage
SAGE = "#a3a473"
# pi.dev: --color-accent-blue / --accent
ACCENT_BLUE = "#6a9fcc"
# pi.dev: --color-tidal-blue / --thread-blue
TIDAL_BLUE = "#4b607c"

# --- Text ---
# 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
109 changes: 109 additions & 0 deletions scripts/build-icon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""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"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {ICON_SIZE} {ICON_SIZE}" width="{ICON_SIZE}" height="{ICON_SIZE}">
<defs>
<clipPath id="lit">
<polygon points="{LIT_CLIP}"/>
</clipPath>
</defs>
<rect x="0" y="0" width="{ICON_SIZE}" height="{ICON_SIZE}" rx="{ICON_RADIUS}" ry="{ICON_RADIUS}" fill="{WARM_BLACK}"/>
<g transform="translate({cx} {cy}) scale({SCALE} -{SCALE}) translate({-gx} {-gy})">
<path d="{d}" fill="{PARCHMENT}"/>
<path d="{d}" fill="{MOONSTONE}" clip-path="url(#lit)"/>
</g>
</svg>
"""


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"""<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {size} {size}" width="{size}" height="{size}">
<g transform="translate({cx} {cy}) scale({SCALE} -{SCALE}) translate({-gx} {-gy})">
<path d="{d}" fill="{WARM_BLACK}"/>
</g>
</svg>
"""


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()
Loading