chore: add agent guide and icon tooling
This commit is contained in:
@@ -0,0 +1,80 @@
|
||||
# GoodBuddy Agent Guide
|
||||
|
||||
## Scope
|
||||
|
||||
These instructions apply to the entire repository.
|
||||
|
||||
GoodBuddy is a secure, cross-platform Electron desktop assistant. It uses
|
||||
Electron, React, TypeScript, Vite, Vitest, and SQLite. User-facing copy is
|
||||
primarily Simplified Chinese.
|
||||
|
||||
## Architecture
|
||||
|
||||
- `src/main`: privileged Electron main process, runtimes, persistence, IPC,
|
||||
knowledge, automation, and OS integration.
|
||||
- `src/preload`: the narrow, typed bridge exposed to the renderer.
|
||||
- `src/renderer`: React UI. It must not receive secrets or direct Node access.
|
||||
- `src/shared`: schemas, contracts, presets, and IPC channel definitions shared
|
||||
across process boundaries.
|
||||
- `resources/skills`: bundled skills.
|
||||
- `build`: packaging scripts and icons.
|
||||
- `out` and `dist`: generated output. Change source files instead.
|
||||
|
||||
Keep Electron security boundaries intact:
|
||||
|
||||
- Preserve context isolation and sandboxing.
|
||||
- Never enable renderer Node integration.
|
||||
- Validate IPC input with shared Zod schemas and verify trusted senders.
|
||||
- Expose only explicit preload methods. Do not pass raw Electron APIs.
|
||||
- Keep API keys in the main process and encrypted settings store.
|
||||
- Never log or return credentials, authorization headers, private documents, or
|
||||
unredacted provider payloads.
|
||||
|
||||
## Runtime Behavior
|
||||
|
||||
- Ask and Plan modes must remain read-only at the runtime boundary.
|
||||
- Execute mode may use tools only through the existing approval controls.
|
||||
- Preserve cancellation, timeout, bounded-output, and shutdown behavior.
|
||||
- Treat OpenCode and Continue as untrusted child runtimes. Preserve environment
|
||||
allowlists, sandbox checks, and per-tool approval enforcement.
|
||||
- A successful image-model configuration check does not prove generation works.
|
||||
Only an actual generation request verifies the provider path.
|
||||
- Do not fetch provider-returned image URLs. Accept and validate bounded inline
|
||||
image data, then persist it as an artifact in the main process.
|
||||
|
||||
## Data and Compatibility
|
||||
|
||||
- Preserve existing user data and migrations.
|
||||
- Do not weaken SQLite transaction, lifecycle, deduplication, or cleanup logic.
|
||||
- Treat user workspaces and untracked files as user-owned.
|
||||
- Keep Windows, macOS, Linux x64, and Linux arm64 behavior in mind.
|
||||
- Do not hard-code machine-specific paths, credentials, or provider endpoints.
|
||||
|
||||
## Implementation Conventions
|
||||
|
||||
- Follow surrounding TypeScript and React patterns.
|
||||
- Reuse installed libraries and shared contracts before adding dependencies.
|
||||
- Keep changes focused. Do not add unrelated refactors or documentation.
|
||||
- Add or update focused tests for behavioral changes and regressions.
|
||||
- Avoid broad catches that erase HTTP status, cancellation, or provider error
|
||||
context. Bound and redact any surfaced error details.
|
||||
- Keep UI accessible with labels, keyboard behavior, semantic roles, and visible
|
||||
focus states.
|
||||
|
||||
## Validation
|
||||
|
||||
Run all validators after source changes:
|
||||
|
||||
```text
|
||||
npm test
|
||||
npm run typecheck
|
||||
npm run lint
|
||||
```
|
||||
|
||||
Run `npm run build` for production build changes. Use `npm run portable` only
|
||||
when a current Windows portable package is requested. Gated runtime tests may
|
||||
make paid or external calls, so run them only with explicit authorization.
|
||||
|
||||
Before committing or pushing, inspect `git status`, `git diff`, and
|
||||
`git diff --cached`. Do not commit secrets, local databases, logs, generated
|
||||
credentials, or private user artifacts.
|
||||
@@ -0,0 +1,96 @@
|
||||
"""Crop the source renders to the icon tile and export PNG sizes plus .ico files."""
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from PIL import Image
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SIZES = [1024, 512, 256, 128, 64, 48, 32, 16]
|
||||
ICO_SIZES = [256, 128, 64, 48, 32, 16]
|
||||
SS = 4
|
||||
|
||||
SOURCES = [
|
||||
("light", ROOT / "ChatGPT_xQBGv24GYc.png"),
|
||||
("dark", ROOT / "ChatGPT_qPkaIrLGsm.png"),
|
||||
]
|
||||
|
||||
|
||||
def tile_mask(rgb):
|
||||
a = rgb.astype(int)
|
||||
r, g, b = a[..., 0], a[..., 1], a[..., 2]
|
||||
# Anything that is not the near-white page background belongs to the artwork.
|
||||
return (a.sum(2) < 748) | (np.abs(r - b) > 4) | (np.abs(g - b) > 4)
|
||||
|
||||
|
||||
def tile_bbox(mask):
|
||||
cols = mask.sum(0)
|
||||
rows = mask.sum(1)
|
||||
xs = np.where(cols > cols.max() * 0.35)[0]
|
||||
ys = np.where(rows > rows.max() * 0.35)[0]
|
||||
x0, x1, y0, y1 = xs.min(), xs.max(), ys.min(), ys.max()
|
||||
# The renders carry a drop shadow below the tile, so trust the width and
|
||||
# square the crop downward from the top edge.
|
||||
side = x1 - x0 + 1
|
||||
return x0, x1, y0, y0 + side - 1
|
||||
|
||||
|
||||
def corner_radius(mask, x0, x1, y0, y1):
|
||||
ests = []
|
||||
for d in range(10, int((y1 - y0) * 0.18)):
|
||||
idx = np.where(mask[y0 + d, x0:x1 + 1])[0]
|
||||
if not len(idx) or idx.min() <= 0:
|
||||
continue
|
||||
x = float(idx.min())
|
||||
ests.append((d + x) + np.sqrt(2.0 * d * x))
|
||||
return float(np.median(ests)) if ests else (x1 - x0) * 0.21
|
||||
|
||||
|
||||
def rounded_alpha(w, h, radius, size):
|
||||
n = size * SS
|
||||
ys, xs = np.mgrid[0:n, 0:n].astype(np.float64)
|
||||
# Map supersampled pixel centres back onto the source tile grid.
|
||||
px = (xs + 0.5) / n * w
|
||||
py = (ys + 0.5) / n * h
|
||||
r = radius
|
||||
dx = np.clip(r - px, 0, None) + np.clip(px - (w - r), 0, None)
|
||||
dy = np.clip(r - py, 0, None) + np.clip(py - (h - r), 0, None)
|
||||
inside = (dx * dx + dy * dy) <= r * r
|
||||
cov = inside.reshape(size, SS, size, SS).mean((1, 3))
|
||||
return (cov * 255).round().astype(np.uint8)
|
||||
|
||||
|
||||
def build(name, path):
|
||||
src = Image.open(path).convert("RGB")
|
||||
mask = tile_mask(np.asarray(src))
|
||||
x0, x1, y0, y1 = tile_bbox(mask)
|
||||
radius = corner_radius(mask, x0, x1, y0, y1)
|
||||
tile = src.crop((x0, y0, x1 + 1, y1 + 1))
|
||||
w, h = tile.size
|
||||
print(f"{name}: tile {w}x{h} radius {radius:.1f}")
|
||||
|
||||
outdir = ROOT / "dist" / name
|
||||
outdir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
frames = {}
|
||||
for size in SIZES:
|
||||
img = tile.resize((size, size), Image.LANCZOS).convert("RGBA")
|
||||
img.putalpha(Image.fromarray(rounded_alpha(w, h, radius, size), "L"))
|
||||
img.save(outdir / f"icon-{size}.png", optimize=True)
|
||||
frames[size] = img
|
||||
|
||||
frames[1024].save(ROOT / "dist" / f"goodbuddy-{name}.ico", format="ICO",
|
||||
sizes=[(s, s) for s in ICO_SIZES])
|
||||
return frames[512]
|
||||
|
||||
|
||||
def main():
|
||||
previews = [build(n, p) for n, p in SOURCES]
|
||||
gap = 32
|
||||
sheet = Image.new("RGBA", (512 * 2 + gap * 3, 512 + gap * 2), (128, 128, 128, 255))
|
||||
for i, img in enumerate(previews):
|
||||
sheet.paste(img, (gap + i * (512 + gap), gap), img)
|
||||
sheet.save(ROOT / "dist" / "preview.png")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user