The resolveElectronExecutable case asserted against the real
node_modules/electron/dist, which bun never downloads without the
electron postinstall, so it failed on macOS/Linux dev machines.
Point the tests at a self-built fixture desktop root instead and
extend coverage to the darwin/linux candidates and the missing-
executable error path.
Follow-up to #1096.
Follow-ups to 9ef170bb, where the discovery path itself is sound but three
surfaces around it are not.
The desktop badge showed the same tooltip for both scopes, and that tooltip
names ~/.agents/skills. A skill that ships inside a checked-out repository was
therefore described as one the user had installed in their own home directory
-- the least trusted source presented as the most trusted. Project-scope
skills now get their own copy, in all five locales.
The doctor listed both .agents targets unconditionally, so a user who set
disableAgentSkillsDirectory (or the env var) still saw them reported, with an
entry count if the directory had contents. That reads as "these skills are in
use" when the loader is ignoring them.
Skill directory names went into the catalog the model sees verbatim, one
`- name: description` line per skill, with no validation. A name containing a
line break writes entries of its own -- and .agents/skills is a directory
other clients and repositories write into, so the name is not necessarily the
user's. Names carrying control characters are now skipped. Deliberately
narrower than the Agent Skills spec's lowercase-and-hyphens rule: existing
.claude/skills directories predate it, and silently dropping one of those
would be the worse failure.
Also collapses the two spellings of the scoped dedup key -- the main loader
joined with a raw NUL byte embedded in the source, the --add-dir path with a
space -- into one helper, so the comment claiming both apply the same rule is
true and the invisible byte is gone.
Adds coverage for the two scopes 9ef170bb claimed but left untested: the
hot-reload watcher (no test file existed for it at all) and on-demand monorepo
discovery. Both were already correct; nothing was holding them that way.
8a3ea62b taught dragging to clamp against the mascot rather than the whole
window, so a saved edge position deliberately leaves the transparent padding
off-screen. Recreating the window re-clamps that position against the whole
window again, and the reposition-on-restore step that undoes it was gated on
darwin -- so on Windows and Linux the mascot walked inwards by the padding
width on every hide/show, not just on restart. The gate only existed because
visibleDragRegion used to be darwin-only; the region now arrives everywhere,
so the repositioning runs everywhere.
e24d59fe restated the recorded window size on every drag tick to stop
setPosition from growing the window, but sampled that size from getBounds().
Chromium converts window rects between physical pixels and DIP with ceil in
both directions, so the sampled value has already been rounded up once and
restating it rounds up again: stable within a drag, a pixel per drag across
them. The window is created non-resizable at the nominal size, so restating
the constants is idempotent instead -- and heals a window that already drifted.
Both call sites now go through movePetWindow.
petWindowContentExtent fell back to the nominal constants when
getContentBounds returned an empty rect, which silently reinstates the exact
clamp it exists to avoid. Fall through to the live window box first.
The fake window in the tests returned one object for both getBounds and
getContentBounds, and modelled setBounds as inherently size-neutral -- so
neither "clamp to the content box" nor "size-neutral drag" was actually
proven: reverting either left all 33 tests green. It now models the ceil/ceil
DIP round trip and carries a distinct content box, and covers the previously
untested paths: cross-platform restore, multi-region shapes, the region
normalization bounds, and the getContentBounds fallbacks.
- Prevent EmptySession crash when settings have not loaded: fall back
to the default permission-mode display and an empty model list, and
stop undefined models responses from poisoning the settings store
- Sync Tauri terminal hardening with Electron: owner binding, input
limits, window-destroy session cleanup, malformed config fallback
- Fix Sheet/MobileBottomSheet z-order so dialogs render above sheets
- Migrate ConfirmPopover to the shadcn Button; remove dead legacy
shared form components and unused mockup pages
- Keep underscores readable in shared diagnostics, redact provider
models only on secret-scanner hits, clean stale WhatsApp login
staging dirs, and add recovery hints for corrupt computer-use config
- Token-ize find-in-page highlight colors, restore the indeterminate
progress slide animation, and tidy a11y/displayName details
Skills installed by Codex, Cursor, Gemini CLI, or opencode live under
`.agents/skills/` -- the convention documented at agentskills.io -- so users
had to keep a duplicate copy in `.claude/skills/` before cc-haha could see
them. The spec only constrains SKILL.md itself, which we already parse, so
this is purely a directory-discovery gap.
Scan both conventions at every scope: user (`~/.agents/skills`), project
(every level up to the repo root), `--add-dir` roots, on-demand monorepo
discovery, and the hot-reload watcher. skillRoots.ts is the single source of
truth for those paths; the REST API now reports which convention a skill came
from and the desktop list badges the `.agents` ones.
`.claude` is always ordered first, so first-wins dedup keeps a user's own
skills authoritative rather than letting an externally installed same-named
skill shadow them. Name collisions are collapsed per scope, which covers
copy-based sync -- symlink-based sync was already handled by the existing
realpath dedup. Collisions across different scopes, such as a user skill and
a project skill sharing a name, keep their existing behavior, and the shadowed
file's identity is recorded so overlapping roots (CLAUDE_CONFIG_DIR pointing
inside the project) cannot reintroduce it under the other scope.
getProjectDirsUpToHome is untouched -- it is shared by commands, agents,
output-styles, and workflows. Only its directory walk is extracted so the
skill resolver reuses the same submodule/worktree stop boundary. Writes stay
where they were: market installs and /skillify still target
`~/.claude/skills`.
`.agents` is anchored at $HOME rather than CLAUDE_CONFIG_DIR, since it is a
space shared with other tools that read $HOME. Note that Bun snapshots
os.homedir() at startup while Node re-reads $HOME per call, so the resolver
reads the env var explicitly.
Opt out with `disableAgentSkillsDirectory` in settings.json or
CLAUDE_CODE_DISABLE_AGENT_SKILLS_DIR=1; no `.agents` root is produced at all
then, so there is not even a failed stat.
(cherry picked from commit ba9338f85d6beaba237753e42e823c87b59633d4)
setInteractiveRegions only recorded visibleDragRegion inside the darwin
branch, so on Windows and Linux it stayed null and dragging fell back to
clamping against the whole PET_WINDOW_WIDTH x PET_WINDOW_HEIGHT window. That
window is mostly transparent padding around a mascot anchored bottom center,
so the mascot stopped short of every display edge by the width of that
padding and could not be dragged into a corner. Record the region on all
platforms; the reposition-on-restore step stays darwin-only.
Normalizing the region also clamped against the nominal constants. Renderer
regions are measured against the live viewport, so clamp against the real
content box instead -- the same petWindowContentExtent the shape clamp
already uses -- and let the shape clamp reuse that one normalizer.
The Windows shape clamp used the nominal PET_WINDOW_HEIGHT constant. Renderer
regions are measured against the live viewport, so once the content area was
taller than that constant the mascot -- which sits flush with the viewport
bottom -- got sliced off from below, while the task badge kept its own
unclamped rect and stayed visible. Clamp the shape to the real content box.
Dragging also moved the window with setPosition, which Windows resolves as
getSize() + setBounds(); that DIP round trip grows the window a pixel at a
time on fractional display scaling, and the drag timer fires every 16ms.
Restate the recorded size on every tick so dragging stays size-neutral.
The svg icon is only a wrapper around app-icon.png, and the nested
reference is invisible to the static copy step, so the image stayed
invisible on production even after the svg itself deployed.
The docs header logo and favicon 404 on production because the
static copy step only scanned markdown for image references. Scan
site sources as well and fail the build when one goes missing.
- rebuild home and docs UI around serif display type, warm paper,
and a single vermilion accent
- rewrite zh/en landing copy around user value
- fix white screen on the last docs page (adjacent-doc out of bounds)
- curate sidebar labels and add TOC scroll-spy
`new URL('..', import.meta.url).pathname` yields a leading-slash path like
/E:/... on Windows, so Bun.spawn failed with ENOENT. Resolve desktopRoot via
fileURLToPath, start Vite through its Node API instead of spawning `bun run
dev`, and resolve the Electron binary directly per platform.
Recovered from the discarded wip/capabilities snapshot (cb1208c9) as a
standalone fix.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>