14 Commits
Author SHA1 Message Date
Mohamed BoudraandGitHub 602860625c Give readable content its own text size (#3637)
* feat(app): separate content typography from interface size

Readable prose benefits from its own scale without inflating navigation and controls. Preserve existing users’ rendered size during migration while giving fresh desktop installs a 15px content default.

* test(app): cover independent content typography

The appearance journey now verifies that changing interface size leaves content size intact. Timeline traversal yields when a test deliberately holds the requested page, so the helper no longer waits on its own gate.

* test(desktop): cover independent content typography

The desktop appearance regression follows the renamed Interface control and verifies it does not alter Content size.
2026-08-21 12:35:23 +02:00
Okan YILDIRIMandGitHub e1805ef542 feat(plugins): add theme contributions
Adds plugin-contributed light and dark app themes, including persistence, host-aware coalescing, fallback behavior, documentation, examples, and browser/iOS QA.
2026-08-21 11:48:12 +02:00
c28836b4e5 feat(app): add Pure black theme (#3012)
* feat(app): add AMOLED and all four Catppuccin themes

Add an AMOLED theme (pure black background) and the four official
Catppuccin flavors (Latte light, Frappé/Macchiato/Mocha dark) to the
appearance picker. Generalize the dark-theme builder to support a light
surface variant, register the new themes with Unistyles, and patch all
registered theme keys when applying font/syntax appearance settings.

* fix(app): localize Catppuccin + AMOLED theme labels across language packs

The initial theme commit pasted the English brand/flavor labels into every
locale. Localize them following each pack's convention: transliterate
Catppuccin and the coffee-flavor names into Arabic/Russian scripts, use
katakana/hanzi equivalents in ja/zh-CN, and standard loanwords in
fr/es/pt-BR. AMOLED is a universal acronym and stays as-is except where
the pack transliterates (Arabic).

* fix(app): make Latte terminal ANSI colors readable

* test(app): cover appearance theme picker options

* refactor(app): scope theme PR to AMOLED

* refactor(app): rename AMOLED theme to Pure Black

* fix(app): refine Pure black appearance

* test(app): wait for sidebar toggle icon

* test(app): remove hover timing race

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-08-09 01:12:53 +02:00
91da0bac4d Choose which orchestration skills get installed (#2680)
* feat(settings): choose which orchestration skills to install

The skills row installed the whole bundle or nothing. A settings control
next to it now opens a sheet where you keep every bundled skill or pick a
subset, and saving converges ~/.agents, ~/.claude, and ~/.codex to it.

"All skills" reads the bundle directory instead of a hardcoded catalog, so
a skill added in a later release installs itself without another code
change. Retired bundle names stay in a separate cleanup list so old copies
on disk are still removed but never become selectable.

The selection persists under Electron user data, separate from install
state, so uninstalling does not erase the choice — a later install brings
back exactly the chosen set. Installs with no saved selection read as
"all", which is what they already had.

* Restore skill selection after failed save

* fix(settings): don't save a skill selection that never reached disk

A failed save wrote the preference, converged, then wrote the old preference
back. That leaves a window where a selection the user was told did not save
is the one on disk, and a crash inside it makes the window permanent.
Converge first and persist only on success — nothing to roll back, and a
failed save now leaves disk and preference exactly as Cancel would.

Resolving the bundle path when the command handlers are wired also broke
daemon-manager's tests: the packaged branch reads process.resourcesPath,
which is undefined outside Electron. Resolve targets per command instead —
registering handlers should not touch the environment.

* fix(settings): make skills convergence single-writer and confirm removals

Startup auto-update and the settings commands were separate writers over the
same skill directories. Either could apply a plan computed from a selection the
other had already replaced — narrow the selection while startup is repairing
drift and the repair puts the removed skill back. There is now one controller
that owns scan, apply, and persist, and every entry point queues through it, so
concurrent callers see a consistent result instead of an interleaved one.

Save also had no recovery once it had converged: if the selection could not be
committed afterwards, disk was changed while the preference still said the old
thing, and the next startup silently undid the change. Save now puts the
managed directories back to what the committed selection describes before it
reports the failure, so a failed Save lands where Cancel would.

Deselecting a skill deletes its directory in all three agent homes, including
files the user put inside it. That is now confirmed like every other
destructive action, naming the skills and the directories. Adding skills or
saving an unchanged selection still saves without a prompt, and a skill the
host has not installed yet deletes nothing so it does not prompt either.

* fix(settings): confirm removal for skills installed in only some agent homes

The removal confirmation read an `add` op as "not on disk yet". An `add` op
means "missing from at least one target", so a skill sitting in ~/.claude but
not the other two looked absent, and deselecting it deleted that directory —
with anything the user had put inside it — without asking.

The status snapshot now carries the installed skill names directly, taken from
union presence across the three agent homes, and the confirmation reads that.
`ops` keeps its one meaning.

A host that predates the field cannot say what is on disk, so the app bridge
assumes the saved selection is installed. Asking about a directory that turns
out to be absent is harmless; the reverse is not.

* fix(settings): warn about every skill directory a save deletes

The confirmation asked "was this selected before and is it selected now",
which is a different question from the one the host answers. The host
converges to the desired set and deletes every managed directory outside it,
so a skill on disk that the saved preference never mentioned — reinstalled by
hand, restored by a sync, left behind by drift — was deleted with no warning,
user files included.

The predicate now mirrors the host rule: everything installed that the draft
will not keep, regardless of what was saved before. It moved out of the sheet
into its own module so the rule has one owner and can be tested directly.
Saving with nothing to delete still does not prompt.

* fix(settings): a failed save keeps files the user added inside a skill

Save converges before it commits, and convergence deletes whole skill
directories. When the commit then failed, the recovery re-installed the
committed selection — which rebuilds the bundled files and nothing else, so
notes, hooks, and anything else the user had put inside a deselected skill
were gone for good. Cancel does not do that, and a failed save is supposed to
land where Cancel would.

Save now runs as a transaction. Before converging it stages a copy of every
managed skill directory across the three agent homes, next to the tree so the
copy stays on one filesystem. Committing drops the staging; failing anywhere
puts each directory back exactly as it was, deletes the ones the candidate had
created, and then drops the staging. Staging left by a process that died
mid-save is cleaned up by the next one.

The controller only sees begin/commit/rollback; the filesystem work lives in
skills-transaction.ts.

* fix(settings): make skill removal confirmation authoritative

* fix(settings): verify skill convergence before commit

* fix(settings): preserve concurrent skill edits on rollback

* fix(settings): preserve staged skill metadata

* fix(settings): stage skill deletions atomically

* fix(settings): stage removals per filesystem

* fix(settings): restore staged skill symlinks

* fix(settings): canonicalize skill transaction roots

* fix(settings): keep automatic skill updates non-destructive

* fix(settings): report committed skill saves accurately

* fix(settings): keep skill updates non-destructive

* fix(settings): route skill removals through selection

* fix(settings): close skill selection edge cases

* fix(settings): preserve skill data during maintenance

* fix(settings): hide unavailable skill install

* fix(desktop): keep install policy at command boundary

* fix(desktop): restore staged skill directories atomically

* test(app): retry transient skills sandbox cleanup

* fix(desktop): recover externally deleted skill stages

* fix(desktop): preserve conflicting skill rollback data

* fix(desktop): generalize skill rollback conflicts

* fix(desktop): preserve skill rollback collisions

* fix(desktop): recover quarantined skill backups

* fix(app): inset adaptive sheet content on compact

The compact Choose skills sheet rendered edge to edge: the All skills card
touched both screen edges, the section label was glued to it, and the skill
list ran to the edges too. Desktop was correct.

AdaptiveModalSheet expressed its content inset through
BottomSheetScrollView.contentContainerStyle. That is a library prop, not the
style prop Unistyles registers, and gorhom's scroller was never rewritten by
the Babel plugin, so on web the emitted class was never applied and the
padding and gap resolved to nothing. The desktop card uses a real ScrollView,
which Unistyles does track, which is why only compact was affected.

The inset now lives on a View the sheet owns, rendered by every presentation,
so one definition covers compact and desktop and a scroller only ever gets
theme-free layout. The caller-facing prop is contentStyle, since it no longer
lands on a scroll content container.

Choose skills drops its ad-hoc label for SettingsSection, so the label carries
the app's section rhythm instead of sitting equidistant between two cards.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(desktop): prevent skill rollback data loss

Preserve externally replaced directory trees during recovery and reject managed child symlinks before synchronization can write outside a skill directory.

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 12:28:32 +02:00
Mohamed Boudra e0e50c9a8e fix(app): preserve persisted theme styles after startup
Module-level style composites could materialize the temporary adaptive theme before persisted settings loaded. Keep Unistyles reads in render and guard against eager module-scope access.
2026-07-18 17:45:02 +02:00
Mohamed BoudraandGitHub 28a27b02d3 Prevent Android chats from freezing or going blank (#1989)
* fix(app): stabilize streamed chat rendering

Use deterministic Markdown AST keys and preserve unique row identities when an assistant message resumes after a tool. Keep native gesture and plan-card children correctly keyed to avoid unsupported Fabric reconciliation paths.

* fix(app): stabilize retained native panels

Keep retained workspace roots in a stable native order and make active
panels present in the selection commit. Hidden panels now stop expensive
subscriptions and animations without losing mounted state.

* fix(app): harden retained panel activity
2026-07-11 15:39:14 +08:00
Mohamed BoudraandGitHub 3163e212fb Attach pull request feedback to chat (#1400)
* feat(app): add pull request panel context actions

Attach pull request comments, reviews, and failed check details to chat context. The PR panel now lives in its own module so the activity, query, and context policies have one home.

* fix(server): mark capped check details as truncated

* fix(app): preserve concurrent PR panel attachments

* fix(server): preserve annotation truncation state

* fix(app): keep matrix check attachments distinct

* fix(app): streamline pull request context attachments

* fix(app): simplify pull request feedback rendering

* feat(app): group pull request review threads

* fix(app): tidy pull request panel boundaries

* fix: use worktree cwd for dev services

* fix(app): keep git query keys UI-free

* feat(app): collapse resolved comments and add chat actions to headers

* fix: address PR feedback and Playwright flakes

* fix(app): hide check chat action without attachment scope

* fix(server): dedupe inline review timeline comments
2026-06-12 13:43:01 +08:00
Mohamed Boudra cd8e0a5e08 Fix sidebar selection and Unistyles warnings
Forward selection state into native draggable lists so virtualized workspace rows update when the active route changes.

Keep floating caller styles array-shaped and strip Unistyles metadata from owned inline geometry to avoid runtime style warnings.
2026-06-04 14:17:08 +07:00
Mohamed Boudra 9f88b4e69c Refine appearance settings and syntax highlighting
Keep parsed chat and tool detail content in sync with appearance token changes. Hide font-family controls on native, use One as the default syntax theme, add Swift highlighting, and give Nord a distinct light palette.
2026-05-31 21:40:38 +07:00
Mohamed BoudraandGitHub a1c8e1a1f9 Add appearance settings for theme, fonts, and syntax highlighting (#1236)
* Add appearance settings for theme, fonts, and syntax highlighting

New Appearance section in Settings to configure the app theme, UI and
mono font family + size, and a syntax-highlighting theme chosen
independently of light/dark. Font family fields default to empty and
show the active system stack as the placeholder; a live split-diff
preview reflects the choices as they change. The theme picker moves
here from General.

Preferences are client-only (AsyncStorage) and applied at runtime by
patching every registered Unistyles theme via
UnistylesRuntime.updateTheme, so existing StyleSheet consumers repaint
with no per-component preference reads and no useUnistyles. No
daemon/protocol change.

* Expand syntax themes and move the preview under the chooser

Replace the small fixed syntax-theme set with eight popular themes — GitHub,
Catppuccin, Dracula, Tokyo Night, One, Nord, Gruvbox, Solarized — built from a
compact per-theme role palette. The app theme and syntax theme stay separate;
the only coupling is the light/dark axis: a theme with both variants uses its
light palette on a light app and its dark palette on a dark app, while the code
frame (gutter, line numbers, background) keeps following the app theme. Default
is now GitHub (GitHub keeps its existing hand-tuned maps for an exact match).

Move the live preview directly under the syntax-theme chooser, drop the
"preview.ts" filename header, and use a clearer code snippet for it.

* Update settings e2e for the moved theme picker and Appearance section

The theme picker moved from General to the new Appearance section, so the
General-content assertion no longer finds "Theme" — point it at "Default send"
(which stays in General) and add Appearance to the e2e section map plus an
expectAppearanceContent check, and exercise the Appearance section in the
sidebar-navigation test.

* Render the appearance preview as a unified diff

Switch the live preview from side-by-side columns to a single unified diff:
unchanged context lines plus "-"/"+" rows for the change, matching how diffs
read elsewhere in the app. Same snippet, mono font, syntax colors, and live
code-font draft behavior.

* Fix code font size change shrinking all UI text

applyAppearance scaled the live (already-patched) theme ramp instead of the
authored FONT_SIZE ramp, so it was not idempotent: every appearance change
re-scaled the current sizes, compounding the whole-UI font scale. With any
non-default UI size, changing the code size — or any setting — shrank (or grew)
all app text, and it persisted into the theme. Build the ramp from the canonical
FONT_SIZE ramp each apply (code size still set absolutely), so applies are
idempotent and a code-size change never touches the UI ramp.

* Apply the interface font across the app on web

react-native-web stamps a default font onto every text element, so the interface
font only reached the few components that set a font explicitly. Inject one rule
that points all text at a CSS variable (updated live as the setting changes),
with high specificity (1,2,0) so it deterministically beats RN-web's base font
and Unistyles' generated classes — no stylesheet-order fragility. Code, diff, and
monospace surfaces carry a `data-pmono` marker (and their subtrees are excluded),
so they keep their code font.

Web-only: React Native has no global font cascade, so native still applies the UI
font only where components opt in. Inline `code` within chat prose is a known
minor gap (its render paths aren't tagged yet).

* Keep preview code in the code font

* Apply code font settings to terminals

* Fix settings host picker import after rebase

* Format terminal webview bundle
2026-05-31 16:09:01 +08:00
Mohamed Boudra 789a559b31 fix(app): make sheet header search text readable in dark mode
The model picker search input rendered text in the light-theme
foreground color on a dark surface, leaving typed characters invisible.

@gorhom/bottom-sheet mounts header subtrees before the sheet is visible
and keeps them mounted across theme changes, so any color read from
StyleSheet.create((theme) => ...) at the caller goes stale. Move text
color and placeholder color into AdaptiveTextInput itself via
withUnistyles, with the leaf's color appended last so callers cannot
re-introduce a stale read. Drop the now-redundant placeholderTextColor
props at the header search call sites.

Also ban useUnistyles() in docs/unistyles.md. The hook subscribes to
every runtime change and forces a re-render each cycle; new code uses
withUnistyles or StyleSheet.create instead.
2026-05-22 18:23:11 +07:00
Mohamed BoudraandGitHub f933bf6b3a fix: stop leaking CSS rules from dynamic UI styles (#1084) (#1103)
* fix: stop leaking CSS rules from dynamic position styles

* Fix floating surfaces on web

* Stop diff controls growing Unistyles rules

* Use inline style escape hatch in diff pane

* Centralize dynamic style escape hatches

* Restore startup workspace navigation

* Upgrade Unistyles for web cleanup fix

* Update review style test for inline styles

* Use redirect for startup workspace restore

* Update startup redirect test
2026-05-20 16:55:00 +08:00
Mohamed Boudra c40e1f03db docs: deep audit pass — fix drift across every doc, rebase coding-standards on /unslop
Each doc verified against current code; stale claims fixed in place.

- architecture: handshake/binary-frame/route/module-table corrections
- data-model: atomic-write claim narrowed, missing daemon files/fields added
- development: db:query removed (SQLite/Drizzle gone), tmux→concurrently+portless, PASEO_HOME split
- unistyles: withUnistyles(Icon) is the dominant pattern; new Animated.View+dynamic-styles iOS gotcha
- SECURITY: bearer-token auth, DNS-rebinding allowlist semantics, ephemeral phone keypair, wire format
- glossary: Project-checkout rename has shipped
- providers: claude-acp not built-in, Pi/mock, async isCommandAvailable, interface drift
- coding-standards: rewritten as compressed /unslop adaptation
- product/release/testing/custom-providers/android/mobile-testing/ad-hoc-daemon-testing/file-icons/design: smaller corrections
2026-05-09 11:35:26 +07:00
Mohamed BoudraandGitHub dec47d72d9 docs: lowercase internal docs + migrate website docs to public-docs/ (#634)
* docs: rename to lowercase + drop leftover plans

Rename docs in docs/ to lowercase kebab-case for consistency and update
all references in CLAUDE.md, CONTRIBUTING.md, CHANGELOG.md,
packages/server/CLAUDE.md, and inter-doc links.

Drop two leftover design plan docs:
- docs/ATTACHMENT_BASED_REVIEW_CONTEXT_PLAN.md
- docs/plan-approval-normalization.md

* docs: drop stale uppercase entries from case-insensitive rename

* feat(website): power /docs from public-docs/ markdown tree

Move website docs out of TSX route components and into a root-level
public-docs/ directory of plain markdown files with frontmatter
(title, description, nav, order).

- Add packages/website/src/docs.ts loader using import.meta.glob with
  ?raw to compile the markdown into the bundle at build time.
- Replace the 9 hand-written docs/*.tsx routes with a single $.tsx
  catch-all that renders any slug via react-markdown.
- Drive the docs sidebar nav from frontmatter order/nav.
- Auto-discover docs routes in vite.config.ts so the sitemap stays in
  sync without manual edits.

* fix(website): bind dev server to 0.0.0.0 so port collisions trigger fallback

`host: "127.0.0.1"` (or unset) lets macOS coexist with another process
holding an IPv6 dual-stack `*:8082` socket, so Vite never sees
EADDRINUSE and silently binds alongside it. Forcing IPv4 wildcard
makes the conflict real, and Vite's default `strictPort: false`
falls through to the next free port.

* fix(website): restore docs page styling after markdown migration

Add a .docs-prose class that mirrors the styling the original
docs/*.tsx components hand-rolled (h1/h2/h3 sizes, paragraph/list
spacing, link colors, code blocks, callout-style blockquotes).

ReactMarkdown was emitting unstyled HTML because the previous
wrapper class only had inline-code rules — headings and code
blocks fell back to user-agent defaults.
2026-04-30 17:27:15 +08:00