open-design/nix/package-daemon.nix
Sid eda182c8a1
refactor(web): UI polish for v0.7.0 — neutralised palette, official brand glyphs, lucide (#1522)
* refactor(web): adopt lucide-react for the inline Icon component

The hand-rolled `<Icon>` set drifted in stroke weight and proportion across
its 50+ glyphs as new icons were added. Swap the implementation to dispatch
to `lucide-react` while keeping the same `<Icon name="..." size={X} />` API
so the 246 existing call sites stay untouched.

- Adds `lucide-react` as a dependency (tree-shaken; ~30KB gzipped for the
  ~50 icons we actually import).
- `discord` and `x-brand` keep their bespoke inline SVG paths since lucide
  intentionally does not ship brand artwork.
- `spinner` continues to use the existing `.icon-spin` className for its
  rotation; under the hood it now renders lucide's `Loader2`.
- New `paw` glyph (lucide `PawPrint`) so the Pets nav item stops sharing
  the `sparkles` icon with External MCP.

No behaviour change: the prop surface is identical, fill follows
`currentColor` exactly as before, and aria-hidden / focusable defaults are
preserved. Visual deltas are limited to the strokes themselves (slightly
finer endcaps, more consistent baseline weights) — exactly the
consistency upgrade lucide gives us.

* feat(web): bundle official brand assets for agent icons

`AgentIcon` previously approximated each agent's brand with hand-drawn
SVG (orange Anthropic-ish sparkle, OpenAI-knot ellipses, etc). Replace
those approximations with the real, vendor-published artwork shipped as
static assets under `apps/web/public/agent-icons/`.

- 13 SVG marks sourced from `@lobehub/icons-static-svg` (MIT) — color
  variants where the vendor published one (Claude, Codex, Gemini,
  Copilot, Qwen, Qoder, DeepSeek, Kimi, Mistral/Vibe), monochrome marks
  for the rest (Cursor, OpenCode, Hermes, MiMo, Pi, Kilo).
- 1 PNG mark (Devin) sourced from devin.ai/icon.png, resized to 96×96
  via `sips` since Cognition doesn't publish an SVG.
- Each SVG was cleaned (stripped `<title>` brand text and the library's
  internal `style="flex:none;..."` ; dropped `width/height="1em"` so
  `viewBox` governs sizing) and run through `svgo --multipass`. Total
  bundle footprint: ~36 KB for all 17 files, only loaded on the agent
  cards that render them.
- `AgentIcon` now resolves brands via a small `ICON_EXT` table and
  renders `<img src="/agent-icons/<id>.<ext>">`. Agents without an asset
  (`devin` is the lone outlier removed in this commit because PNG; new
  agents with no shipped artwork at all) fall back to an initial-letter
  pill that reads as "no official mark yet" rather than inventing
  brand artwork.
- Removes the `simple-icons` dependency from a previous iteration since
  `AgentIcon` was its only consumer.

Public-API stable: `<AgentIcon id={a.id} size={X} />` still accepts the
same prop shape; `AvatarMenu`'s small-size usage continues to work.

* refactor(web): polish entry view + Settings dialog UI for v0.7.0

A sweep over the two surfaces that have the most visual surface area in
the app (the entry sidebar / New Project panel on the left, and the
Settings modal). The work converged on a single neutral palette + a
small set of shared dimensional standards documented in CSS, so future
sections that get added slot into the same rhythm.

New Project panel (apps/web/src/components/NewProjectPanel.tsx +
.newproj* rules in index.css)
- Adds a spec comment block at the top of the .newproj rules listing
  the canonical heights (input 30, dropdown 38, compact toggle 36,
  popover item 38) and the neutral colour rules.
- Rebuilds PlatformPicker as a DS-picker-style dropdown trigger +
  popover (the previous 6-card 2×3 grid was ~280px tall; the dropdown
  collapses to a single 38px row with the same multi-select semantics).
- Replaces SurfaceOptions' two heavy `ToggleRow` cards with the new
  compact one-line `CompactToggle`; the descriptive hint moves to a
  native `title` tooltip.
- Compresses the Fidelity card grid (thumb aspect 16/7 → 16/5, tighter
  padding, smaller label).
- Neutralises every selected/active state inside the panel: removes the
  orange accent fills and rings from `.newproj-card.active`,
  `.newproj-title-badge`, `.compact-toggle.on`, `.toggle-row.on`, the
  DS picker popover items + radio/check marks, the trigger open border
  and shadow, and the search-bar background. The Create CTA stays the
  only orange element on the panel.
- Aligns the project-name input focus state across the sidebar:
  border `var(--text)` + 8% black halo (rgba is written out because the
  CSS pipeline collapses `color-mix(... 8%, transparent)` down to a
  solid `var(--text)`, which would render as a 3px solid black band).
- Switches the body card from `flex: 1 1 auto` to `flex: 0 1 auto` so a
  short form variant doesn't leave a white void at the bottom of the
  card, and disables overscroll-bounce on the card so a fast scroll
  doesn't briefly expose the page-level gray under the white surface.
- Pins the privacy footer below the card with a fixed 0 margin-top +
  shorter padding-top so it reads as a label of the card rather than a
  centred dialog footer.

Entry sidebar footer (apps/web/src/components/EntryView.tsx +
.entry-side-foot* rules)
- Replaces the X social pill's `external-link` placeholder glyph with a
  bespoke filled `x-brand` SVG that mirrors the `discord` mark already
  in the icon set.
- Wraps Discord + X in `.entry-side-foot-social` and lets that group
  flex-margin to the right of the row, so the two social pills read as
  a tight pair instead of a fourth pill stuck to the Pet pill.
- Drops the "unadopted" red dot on the Pet pill (it duplicated the call
  to action that the label already carried).
- Shrinks the footer icons to 10px and dims them to 55% / 75% opacity
  on hover so the labels are clearly the focal point — `currentColor`
  on the lucide-rendered SVGs would otherwise make the glyphs full
  black on hover.
- Tightens the env-pill version text cap (180 → 142) so the top row
  ends close to the right edge of the Language + Pet group below it.

Settings dialog (apps/web/src/components/SettingsDialog.tsx +
.modal-settings / .settings-* / .seg-* / .agent-* rules)
- Removes the "SETTINGS" kicker eyebrow above each section title (the
  big-typography title and modal context already make it redundant).
- Switches the sidebar from a card-per-item layout to ChatGPT-style
  single-line pills: hides the `<small>` description, swaps the
  sidebar bg from gray to white, makes the active item a gray pill (no
  border, no shadow) so all items keep a consistent row height
  regardless of state.
- Drops the modal-body's top border (already separated by the
  whitespace between modal-head and the body grid) and pins
  `.modal-settings { height: min(720px, 100vh - 64px) }` so the
  dialog no longer resizes when the user switches between short and
  long sections.
- Compresses the Local CLI / BYOK seg-control from a 2-line ~52px card
  pair to a 1-line ~42px segmented pill that height-matches the active
  sidebar nav-item, and aligns the `.settings-content` padding-top
  with `.settings-sidebar` (22 → 16) so the first content row sits
  level with the first sidebar item.
- Neutralises agent-card selected state, install/docs link colour, and
  protocol-chip active state — same accent-stripping pattern as the
  New Project panel.
- Uniform agent-card height via `min-height: 64px` so installed cards
  (icon + name + version) align with unavailable cards (icon + name +
  not-installed + Install/Docs row).

No prop-API changes, no business-logic edits — this is a pure visual
refactor. Existing tests, providers and daemon contracts are untouched.
2026-05-13 13:59:19 +08:00

178 lines
6.3 KiB
Nix

{
lib,
stdenv,
dream2nix,
nixpkgs,
system,
nodejs,
pnpm_10,
fetchPnpmDeps,
pnpmConfigHook,
src,
makeWrapper,
python3,
gnumake,
pkg-config,
}:
# Builds the @open-design/daemon workspace package — produces $out/bin/od.
#
# Implementation note on dream2nix:
# The flake takes `dream2nix` as an input (per the project's Nix
# contract) but the build itself uses stdenv.mkDerivation. dream2nix's
# nodejs builders consume npm's package-lock.json — they have no
# first-class pnpm-lock.yaml + workspace builder yet. When upstream
# ships one, swap this file for a thin dream2nix module — the inputs
# are already wired.
#
# pnpm version note:
# `package.json` declares `engines.pnpm: ">=10.33.2 <11"` and pnpm
# enforces this on `pnpm install` (regardless of `engine-strict`).
# nixpkgs currently ships 10.33.0, which is rejected. The flake
# overrides `pkgs.pnpm_10` to fetch the 10.33.2 tarball from npm —
# see flake.nix for the override and how to bump the hash when
# `packageManager` advances.
#
# Workspace siblings the daemon depends on (contracts, sidecar-proto,
# sidecar, platform) are built in dependency order before the daemon
# itself; tsc emits each package's dist/, which is what the daemon
# resolves at runtime via pnpm's symlinked node_modules.
let
pname = "open-design-daemon";
version = (lib.importJSON ../package.json).version;
# Vendored pnpm store. The hash MUST be pinned on first build:
# `nix build .#daemon` will fail with the expected hash printed; copy
# that into `pnpmDepsHash` below. Bump it whenever pnpm-lock.yaml
# changes.
pnpmDepsHash = "sha256-/C9tl0CY/vbDr369wVowsrEMxhljX0pnW7kGZbz3Fas=";
# pnpmDepsHash = lib.fakeHash;
in
stdenv.mkDerivation (finalAttrs: {
inherit pname version src;
nativeBuildInputs = [
nodejs
pnpm_10
pnpmConfigHook
makeWrapper
# Required to rebuild better-sqlite3's native binding from source.
# node-gyp drives this via Python; gnumake/pkg-config + the C++
# compiler from stdenv complete the toolchain.
python3
gnumake
pkg-config
];
pnpmDeps = fetchPnpmDeps {
inherit (finalAttrs) pname version src;
hash = pnpmDepsHash;
fetcherVersion = 3;
};
env.NODE_ENV = "production";
# pnpm_10.configHook runs in postConfigureHooks: it unpacks
# `pnpmDeps`, points pnpm at the unpacked store, and runs
# `pnpm install --offline --ignore-scripts --frozen-lockfile`.
# No custom configurePhase needed.
buildPhase = ''
runHook preBuild
# Build better-sqlite3's native binding from source.
#
# Why from source on Node 24:
# better-sqlite3 (even 12.9.0, latest as of 2026-05) only
# publishes prebuilds up to node-v131 (Node 22). No v137
# (Node 24) prebuild exists. `prebuild-install` would itself
# fail the GitHub fetch and fall through to a compile, so we
# skip the download attempt entirely and compile.
#
# Why not `pnpm rebuild`:
# In pnpm 10, `onlyBuiltDependencies` interacts with the
# "approve-builds" consent gate; `pnpm rebuild <pkg>` silently
# no-ops in some configurations. Invoke node-gyp directly to
# sidestep all of that.
#
# Env vars:
# * npm_config_nodedir use the headers shipped with the
# nixpkgs nodejs we're already building against, so node-gyp
# doesn't try to fetch them from nodejs.org/dist (no network
# in the build sandbox).
# * npm_config_build_from_source tell better-sqlite3's
# prebuild-install fallback chain to skip the CDN download
# and compile.
#
# node-gyp lookup:
# nixpkgs nodejs ships node-gyp bundled inside npm at
# ${nodejs}/lib/node_modules/npm/bin/node-gyp-bin. Putting
# that on PATH gives us a `node-gyp` shim without depending
# on pnpm-exec resolving from inside better-sqlite3's tree
# (better-sqlite3 doesn't list node-gyp as a direct dep).
export npm_config_nodedir=${nodejs}
export npm_config_build_from_source=true
export PATH="${nodejs}/lib/node_modules/npm/bin/node-gyp-bin:$PATH"
bsq_dir=$(find node_modules/.pnpm -mindepth 2 -maxdepth 4 \
-type d -path '*/better-sqlite3@*/node_modules/better-sqlite3' \
-print -quit)
if [ -z "$bsq_dir" ]; then
echo "ERROR: better-sqlite3 not found under node_modules/.pnpm pnpm install may have failed" >&2
exit 1
fi
echo "Building better-sqlite3 from source at $bsq_dir"
( cd "$bsq_dir" && node-gyp rebuild --release --build-from-source )
# Fail fast if the .node file didn't land where bindings.js
# looks for it. Without this assertion, a silent skip produces
# a "valid" derivation that crashes at runtime with
# "Could not locate the bindings file".
if [ ! -f "$bsq_dir/build/Release/better_sqlite3.node" ]; then
echo "ERROR: better_sqlite3.node was not produced at $bsq_dir/build/Release/" >&2
find "$bsq_dir" -name '*.node' -print >&2 || true
exit 1
fi
for target in \
packages/contracts \
packages/sidecar-proto \
packages/sidecar \
packages/platform \
apps/daemon
do
pnpm -C "$target" run --if-present build
done
runHook postBuild
'';
installPhase = ''
runHook preInstall
mkdir -p $out/lib/open-design $out/bin
# Copy the whole workspace tree pnpm's symlinks under node_modules
# resolve sibling packages by relative paths, so we cannot prune to
# just apps/daemon.
cp -r . $out/lib/open-design/
chmod +x $out/lib/open-design/apps/daemon/dist/cli.js
makeWrapper ${nodejs}/bin/node $out/bin/od \
--add-flags $out/lib/open-design/apps/daemon/dist/cli.js \
--set NODE_ENV production
runHook postInstall
'';
passthru = {
inherit nodejs;
pnpmDeps = finalAttrs.pnpmDeps;
};
meta = with lib; {
description = "Open Design daemon local agent orchestrator + API (`od` CLI)";
homepage = "https://github.com/nexu-io/open-design";
license = licenses.asl20;
mainProgram = "od";
platforms = platforms.linux ++ platforms.darwin;
};
})