# edit_theme

Change one of the shooter's own themes: colors, fonts, text size, corner and spacing style, background.

## What it is for

`edit_theme` changes one of the shooter's own themes (colors, fonts, text size, background) and saves the result at once as the theme's next version. A theme is the look, not the arrangement ([the family page](appearance.md)). The tool makes no new theme, previews nothing, and does not choose what a device shows.

## What to know first

**It builds on the newest version of the theme, and a built-in is refused.** Give a theme id, or a name only one record has (built-ins count). A version written in `theme_id` (`Dusk@1`) is refused. For a built-in, `create_theme` makes the editable copy.

**The save is immediate.** There is no preview and no Keep step. A device using the theme picks the new version up through sync. To undo, use `restore_version`; to try first, `preview_appearance`.

**The change is merged, except two members.** `colors` merge token by token; `background` and `dim` are replaced whole (`dim: null` removes it); the other members replace the stored value. An unknown key inside `change` is refused.

**A picture must be one of the account's own theme images.** An image background is `asset:` and the picture's hash from `list_assets`. A hash that list does not hold is refused.

## Generated table

**Tool:** `edit_theme` · **Family:** Appearance · **Scope:** `appearance` · **Save scope:** none · **Changes:** records · **Runs:** no solver work in the connector

**Description** (what the connector sends to an assistant, word for word):

```text
Change one of the shooter's own themes: colors, fonts, text size, corner and spacing style, background. To try a change without saving use preview_appearance; to undo, restore_version; a built-in can't be edited, create_theme copies one. Saves the next version at once, with no preview; needs the appearance permission. Give only what changes: colors merge token by token, while background and dim are replaced whole. Guide: edit_theme.
```

**Inputs** (3; read from the input schema the connector sends):

| Input | Type | Required | Bounds | Field |
|---|---|---|---|---|
| `theme_id` | text | yes | - | - (selects a record) |
| `change` | object | yes | - | - (appearance record) |
| `change.name` | text | no | 1 to 60 characters | `name` (theme) |
| `change.colors` | object with keys from `"bg"`, `"panel"`, `"panel_alt"`, `"text"`, `"muted"`, `"accent"`, `"on_accent"`, `"border"`, `"input_bg"`, `"warn"`, `"warn_bg"`, `"ok"`, `"claude"`, `"wind_left"`, `"wind_right"`, `"focus"`, each text | no | - | `colors` (theme) |
| `change.font_body` | `"system"` or `"inter"` or `"atkinson"` or `"b612"` or `"plex_sans"` or `"roboto_mono"` or `"jetbrains_mono"` | no | - | `font_body` (theme) |
| `change.font_numbers` | `"system"` or `"inter"` or `"atkinson"` or `"b612"` or `"plex_sans"` or `"roboto_mono"` or `"jetbrains_mono"` | no | - | `font_numbers` (theme) |
| `change.base_size_px` | number | no | - | `base_size_px` (theme) |
| `change.radius` | `"square"` or `"soft"` or `"round"` | no | - | `radius` (theme) |
| `change.density` | `"tight"` or `"normal"` or `"roomy"` | no | - | `density` (theme) |
| `change.background` | object (other keys allowed) | no | - | `background` (theme) |
| `change.background.kind` | `"solid"` or `"gradient"` or `"effect"` or `"image"` | yes, within | - | `background.kind` (theme) |
| `change.panel_opacity` | number | no | - | `panel_opacity` (theme) |
| `change.dim` | object or null | no | - | `dim` (theme) |
| `change.dim.color` | `"black"` or `"white"` | yes, within | - | `dim.color` (theme) |
| `change.dim.amount` | number | yes, within | - | `dim.amount` (theme) |
| `request` | text | yes | 1 to 300 characters | `note` (theme) |

**Sent with the inputs** (each input's own description, word for word; an input not listed is sent without one):

```text
theme_id: One of the shooter's own themes: its id from list_appearance, or a name only one theme has; no @version
change: What to change; a field not given stays as it is
change.name: A new name for the theme
change.colors: Color tokens to change, each "#rrggbb"; the others stay
change.font_body: Font for text
change.font_numbers: Font for numbers
change.base_size_px: Text size, whole pixels, 14–22
change.radius: Corner style
change.density: Spacing
change.background: Replaces the whole background. background is one of: {kind: "solid"}; {kind: "gradient", stops: 2–8 "#rrggbb", angle_deg, style: "linear"|"stripes", motion: "none"|"hue_drift"|"scroll", speed 0.1–3}; {kind: "effect", effect, params} with matrix_rain (color, speed, density, glyphs); starfield (color, speed, density); rainbow_wave (speed, saturation); {kind: "image", asset: "asset:<sha256>" from list_assets, fit: "cover"|"contain"|"tile", position: "center", blur_px 0–40}. Images can never come from a URL.
change.panel_opacity: Cards over the background, 0–1
change.dim: Overlay over the background, amount 0–0.8; null removes it
request: The shooter's request in their own words; stored as the version's note, so "undo that" can find it
```

## When to use it, and when not

| The shooter wants | Use | Why |
|---|---|---|
| A look changed and kept | `edit_theme` | Saves the next version at once |
| To see a change before keeping it | [preview_appearance](preview_appearance.md) | Shown on one device; nothing saved |
| A built-in look changed, or a second copy to work on | [create_theme](create_theme.md) | Built-ins cannot be edited |
| The last change undone, or an old look back | [restore_version](restore_version.md) | Brings an old version back as the newest |
| What a theme holds now, or its versions | [get_theme](get_theme.md) | Reads, changes nothing |
| Sections or widgets moved | [edit_layout](edit_layout.md) | The arrangement is a layout |

Ask which theme if the shooter has not said and `list_appearance` shows more than one. Send the shooter's own words as `request`.

## The inputs

**`theme_id`** selects the theme: its id or its name, in any case. A name that two records share (an own theme and a built-in count as two) is answered as no such theme; use the id. An id ending in `@` and a number is refused.

**`change`** holds at least one member, or the tool answers `Tell me what to change.` The saved theme must pass the app's theme rules, whole: a value outside them is refused with the rule's words, and the refusal lists every rule broken. The rules are on [the family page](appearance.md). The members:

- **`change.name`** renames the theme. It is saved as typed: spaces are not trimmed, and a name another theme or a built-in already has is accepted (that theme is then reached by id only).
- **`change.colors`** merges token by token and keeps every token not named. A token that does not exist is refused by the input check.
- **`change.font_body`**, **`change.font_numbers`**, **`change.radius`** and **`change.density`** replace the stored choice; a word outside the listed choices is refused by the input check.
- **`change.base_size_px`** and **`change.panel_opacity`** replace the stored number; the theme rules refuse one out of range (text size 14 to 22 whole pixels, opacity 0 to 1).
- **`change.background`** and **`change.background.kind`**: the background replaces the old one, so give all of its members. The tool checks `kind`, a gradient's stops, speed, `style` and `motion`, an effect and its parameters, and an image's `asset`, `fit` and `blur_px`. A member the kind does not have is refused (`a solid background has no colour`), and so is a `style`, `motion` or `fit` that is not a listed choice. Only the background given in the call is checked this way: a stored theme is not refused for what it already holds.
- **`change.dim`**, **`change.dim.color`** and **`change.dim.amount`**: the dim layer is replaced whole (amount 0 to 0.8 by the theme rules); `null` removes it.

**`request`** is the shooter's words, stored as the new version's note. It is the only way the version is described later, so keep it short and true.

## What it returns

On success the spoken line is `Saved '<name>' as v<N>. Devices using it update within a few seconds.` and the details hold `theme_id` and `version`. The name is the one the theme has after the edit. A name stored with several words is spoken in quotation marks with the stored-text note; the name the call itself just set is not (run). If another device saved a version at that number first, the server renumbers and the reply gives the number the save got (the family page's run).

| Key | Holds |
|---|---|
| `theme_id` | The theme's id, the same for every version |
| `version` | The number of the version this call saved |

## Worked examples

Every example below was run by the documentation generator against a made-up account, never a real one. Numbers are rounded to three places; generated ids and times are shown as placeholders. Two things are printed more briefly than the tool sent them, and nothing else is changed. A list of plain values stands on one line, whole up to 12 entries (a longer one, and a list of objects past 3, ends with a count of the rest). A reply that puts stored text in quotation marks also carries a note saying so, which an example leaves out and marks instead; no reply below carried it.

## When it refuses

| Refusal | Cause | Tell or ask the person |
|---|---|---|
| `Tell me what to change.` | `change` has no member | Ask what to change |
| `Edits apply to the latest version; restore_version brings back an old one.` | `theme_id` ends in `@` and a number | Use the theme's name or id alone; for an old version use `restore_version` |
| `No theme ` followed by the id as given and `. list_appearance shows the themes.` | No theme matches, or two records have that name | Look at `list_appearance`; use the id |
| ` is built in and can't be changed. create_theme from it makes an editable copy.` | A built-in (its id starts `builtin:`) | Offer `create_theme` |
| `Nothing saved — ` and the rules the result breaks, joined with `; ` | A value is outside the theme rules, for example `base_size_px must be 14–22, got 23` | Say which value and its range |
| `Nothing saved — ` and then `there's no background image ` and the reference, ` in your account; list_assets shows what is there.` | The result's image is not in `list_assets`. The check is on the whole result, so a theme whose stored picture has left the account is refused for any change that keeps that picture (a rename, a size); a change that replaces the background is saved | Show `list_assets`; a picture is uploaded in the app |
| `Nothing saved — ` and the server's reason in quotation marks | The server turned the save down (for example `this account's storage is full`) | Say nothing was saved |

When the picture came from the stored theme, the reference is spoken in quotation marks, cut to 40 characters, with the stored-text note; a hash the caller typed is not (run, both).

The input check turns away a name of no characters or of more than 60, a request of none or of more than 300, a `kind`, font, radius or density that is not a listed choice, a missing `theme_id` or `request`, and an unknown key inside `change`; the rule is on [the connector overview](connector.md). An input the tool does not have, at the top level, is dropped, and the reply names it (since 2026-10-15). On a link without the scope `appearance` the tool is not offered (run).

## What it reads and changes

**Reads** the account's themes (the newest live version of the one named) and, only when the result has an image background, the list of uploaded pictures. The picture check runs on the whole result: a theme whose stored picture has left the account is refused for any change that keeps that picture, and saved if the change replaces the background (run).

**Saves** one new version of the same theme: the number after its newest, `source` Claude, the request as note, and the same `based_on` as the version it started from. Older versions stay. A change that alters nothing (`colors: {}`, or a value already stored) still saves a version. It makes no theme, sends no command to a device and does not change what any device shows.

## Below this page

- [list_assets](list_assets.md): the pictures an image background can use.
- [the connector overview](connector.md), [the family page](appearance.md)

## Worked examples

**1. Larger numbers**

Starting point: one theme and one layout of the shooter's, and one phone. Link scopes: `read`, `appearance`.

Call:
```json
{"theme_id":"Dusk","change":{"base_size_px":18},"request":"larger numbers"}
```

Spoken line:
```text
Saved 'Dusk' as v2. Devices using it update within a few seconds.
```

Details:
```json
{
 "theme_id": "fixture-theme",
 "version": 2
}
```

Saved the theme at version 2, changed: `base_size_px`, `note`, `source`.

**2. Two colors, a font and a dim layer**

Starting point: one theme and one layout of the shooter's, and one phone. Link scopes: `read`, `appearance`.

Call:
```json
{"theme_id":"Dusk","change":{"colors":{"accent":"#ff8800","bg":"#101820"},"font_numbers":"b612","dim":{"color":"black","amount":0.4}},"request":"warmer and darker"}
```

Spoken line:
```text
Saved 'Dusk' as v2. Devices using it update within a few seconds.
```

Details:
```json
{
 "theme_id": "fixture-theme",
 "version": 2
}
```

Saved the theme at version 2, changed: `colors.accent`, `colors.bg`, `dim.amount`, `dim.color`, `font_numbers`, `note`, `source`.

**3. The uploaded image as the background**

Starting point: a theme at version 2, a layout, one uploaded image of 48 kB, two share links (one for the theme, and a later one for the layout that was stopped) and one phone. Link scopes: `read`, `appearance`.

Call:
```json
{"theme_id":"Dusk","change":{"background":{"kind":"image","asset":"asset:c07e5a1934fb268dc07e5a1934fb268dc07e5a1934fb268dc07e5a1934fb268d","fit":"cover","blur_px":4}},"request":"harbour picture behind"}
```

Spoken line:
```text
Saved 'Dusk' as v3. Devices using it update within a few seconds.
```

Details:
```json
{
 "theme_id": "fixture-theme",
 "version": 3
}
```

Saved the theme at version 3, changed: `background.asset`, `background.blur_px`, `background.fit`, `background.kind`, `note`, `source`.

**4. An image the account does not hold**

Starting point: a theme at version 2, a layout, one uploaded image of 48 kB, two share links (one for the theme, and a later one for the layout that was stopped) and one phone. Link scopes: `read`, `appearance`.

Call:
```json
{"theme_id":"Dusk","change":{"background":{"kind":"image","asset":"asset:4b1d09e2a7c35f684b1d09e2a7c35f684b1d09e2a7c35f684b1d09e2a7c35f68","fit":"cover","blur_px":4}},"request":"other picture"}
```

The tool refuses:
```text
Nothing saved — there's no background image asset:4b1d09e2a7c35f684b1d09e2a7c35f684b1d09e2a7c35f684b1d09e2a7c35f68 in your account; list_assets shows what is there.
```

**5. A value the theme rules turn down, and an empty change**

Starting point: one theme and one layout of the shooter's, and one phone. Link scopes: `read`, `appearance`.

Call 1:
```json
{"theme_id":"Dusk","change":{"base_size_px":23},"request":"much larger"}
```

The tool refuses:
```text
Nothing saved — base_size_px must be 14–22, got 23
```

Call 2:
```json
{"theme_id":"Dusk","change":{},"request":"nothing"}
```

The tool refuses:
```text
Tell me what to change.
```

**6. A built-in theme, and a version written in the id**

Starting point: one theme and one layout of the shooter's, and one phone. Link scopes: `read`, `appearance`.

Call 1:
```json
{"theme_id":"builtin:dark","change":{"radius":"round"},"request":"rounder"}
```

The tool refuses:
```text
Dark is built in and can't be changed. create_theme from it makes an editable copy.
```

Call 2:
```json
{"theme_id":"Dusk@1","change":{"radius":"round"},"request":"rounder"}
```

The tool refuses:
```text
Edits apply to the latest version; restore_version brings back an old one.
```

**7. A name two themes have, then the id of one**

Starting point: three themes of the shooter's (two named Dusk, and one named Daylight, as a built-in theme is), three layouts (two named Bench, and one named Compact, as a built-in layout is) and one phone. Link scopes: `read`, `appearance`.

Call 1:
```json
{"theme_id":"Dusk","change":{"density":"roomy"},"request":"roomier"}
```

The tool refuses:
```text
No theme Dusk. list_appearance shows the themes.
```

Call 2:
```json
{"theme_id":"fixture-theme-b","change":{"density":"roomy"},"request":"roomier"}
```

Spoken line:
```text
Saved 'Dusk' as v2. Devices using it update within a few seconds.
```

Details:
```json
{
 "theme_id": "fixture-theme-b",
 "version": 2
}
```

Saved the theme at version 2, changed: `density`, `note`, `source`.

**8. A change that changes nothing still saves a version**

Starting point: one theme and one layout of the shooter's, and one phone. Link scopes: `read`, `appearance`.

Call:
```json
{"theme_id":"Dusk","change":{"colors":{}},"request":"nothing"}
```

Spoken line:
```text
Saved 'Dusk' as v2. Devices using it update within a few seconds.
```

Details:
```json
{
 "theme_id": "fixture-theme",
 "version": 2
}
```

Saved the theme at version 2, changed: `note`, `source`.

**9. A rename to the name another theme has**

Starting point: three themes of the shooter's (two named Dusk, and one named Daylight, as a built-in theme is), three layouts (two named Bench, and one named Compact, as a built-in layout is) and one phone. Link scopes: `read`, `appearance`.

Call:
```json
{"theme_id":"fixture-theme-day","change":{"name":"Dusk"},"request":"a new name"}
```

Spoken line:
```text
Saved 'Dusk' as v2. Devices using it update within a few seconds.
```

Details:
```json
{
 "theme_id": "fixture-theme-day",
 "version": 2
}
```

Saved the theme at version 2, changed: `name`, `note`, `source`.

**10. A name of spaces**

Starting point: one theme and one layout of the shooter's, and one phone. Link scopes: `read`, `appearance`.

Call:
```json
{"theme_id":"Dusk","change":{"name":"   "},"request":"blank name"}
```

Spoken line:
```text
Saved '   ' as v2. Devices using it update within a few seconds.
```

Details:
```json
{
 "theme_id": "fixture-theme",
 "version": 2
}
```

Saved the theme at version 2, changed: `name`, `note`, `source`.

**11. A theme with several versions: the edit builds on the newest**

Starting point: one theme whose stored versions are 1 (named Dawn, saved in the app), 2 (deleted), 3 (named Dusk, saved by Claude with the note larger text) and 4 (named Dusk, saved in the app); one layout at versions 1 and 2, the second with a title on its first section; and one phone. Link scopes: `read`, `appearance`.

Call:
```json
{"theme_id":"Dusk","change":{"radius":"soft"},"request":"softer"}
```

Spoken line:
```text
Saved 'Dusk' as v5. Devices using it update within a few seconds.
```

Details:
```json
{
 "theme_id": "fixture-theme",
 "version": 5
}
```

Saved the theme at version 5, changed: `note`, `source`.

---

[All reference pages](index.md)
