mirror of
https://github.com/esengine/DeepSeek-Reasonix.git
synced 2026-11-07 10:45:59 -10:00
docs(studio): add current theme author guide and starter
This commit is contained in:
1 parent
1e6b185c2d
commit
29791a36c6
7 files changed
+332
-18
No files matched your search
@@ -10,6 +10,7 @@
|
||||
|
||||
# User-facing guides and references.
|
||||
/docs/ @SivanCola @esengine
|
||||
/docs/THEME_AUTHOR_GUIDE.md @esengine @SivanCola
|
||||
/sdk/ @SivanCola @esengine
|
||||
/README.md @SivanCola @esengine
|
||||
/README.zh-CN.md @SivanCola @esengine
|
||||
|
||||
@@ -27,6 +27,9 @@ OpenAPI inputs, a reference checker, and a source-linked guide format.
|
||||
The [stdio MCP example](../examples/mcp-line-counter-kit/README.md) bundles a
|
||||
local Go server with build, connection, tool-call, and lifecycle checks.
|
||||
|
||||
The [Studio theme author guide](THEME_AUTHOR_GUIDE.md) uses a copyable pure-theme
|
||||
package, the current token vocabulary, and Appearance activation and cleanup.
|
||||
|
||||
## CLI Mode
|
||||
|
||||
Use `reasonix plugin` when installing or managing plugin packages from a
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
---
|
||||
status: active
|
||||
owner: @esengine
|
||||
backup: @SivanCola
|
||||
reviewed: 2026-10-01
|
||||
---
|
||||
|
||||
# Studio theme author guide
|
||||
|
||||
Studio reads a directory containing `theme.json` with `schemaVersion: 1`.
|
||||
The [Paper Dawn package](themes/reasonix-plugin.json) is a local, token-only
|
||||
starter with light and dark palettes. Its palette is released as CC0-1.0.
|
||||
It contributes one theme and no skills, hooks, commands, MCP servers or runtime.
|
||||
|
||||
## Install and try the starter
|
||||
|
||||
From this repository's root, with the Studio `reasonix` CLI on your PATH:
|
||||
|
||||
```bash
|
||||
reasonix plugin install ./docs/themes --dry-run
|
||||
reasonix plugin install ./docs/themes --yes
|
||||
reasonix plugin doctor paper-dawn-kit
|
||||
reasonix plugin show paper-dawn-kit
|
||||
```
|
||||
|
||||
Review the preview before installing: it should contain one plugin action with
|
||||
`themeCount: 1` and no execution capabilities. Installation copies the package;
|
||||
moving the source afterwards does not break the installed theme.
|
||||
|
||||
Open **Settings → Appearance** in Studio and choose **Paper Dawn** from the theme
|
||||
choices. Installing a theme does not select it. Try both light and dark modes,
|
||||
read ordinary and muted text, and check a dialog and a code block.
|
||||
|
||||
The package's theme identity is `plugin:paper-dawn-kit:paper-dawn`: the plugin
|
||||
name and the contributed theme directory name determine it. The `id` inside
|
||||
`theme.json` does not override that plugin identity.
|
||||
|
||||
## Make your own package
|
||||
|
||||
Copy `docs/themes` into a local working folder; keep this layout:
|
||||
|
||||
```text
|
||||
my-palette/
|
||||
reasonix-plugin.json
|
||||
paper-dawn/
|
||||
theme.json
|
||||
```
|
||||
|
||||
Change the plugin `name`, `version` and `description` in
|
||||
`reasonix-plugin.json`, then edit the theme's `name`, `author`, `description`
|
||||
and palette. If you rename `paper-dawn`, update `contributes.themes` to match.
|
||||
The manifest uses an explicit relative path; all contributions stay inside
|
||||
the package root.
|
||||
|
||||
For a live local development copy:
|
||||
|
||||
```bash
|
||||
cd /absolute/path/to/my-palette
|
||||
reasonix plugin install . --link --yes
|
||||
```
|
||||
|
||||
A link source must be inside the current workspace or your home directory.
|
||||
A linked package reads your working folder. Keep it in place, and keep the
|
||||
theme directory name stable.
|
||||
|
||||
After edits, reopen Studio's Appearance settings
|
||||
and reselect the theme to reread its tokens. For a copied installation, preview
|
||||
and apply the replacement instead:
|
||||
|
||||
```bash
|
||||
reasonix plugin install /absolute/path/to/my-palette --replace --dry-run
|
||||
reasonix plugin install /absolute/path/to/my-palette --replace --yes
|
||||
```
|
||||
|
||||
Use your package's manifest name in management commands. Share an immutable
|
||||
repository commit using the [community author guide](MARKET_AUTHOR_GUIDE.md)
|
||||
after checking the package and its licence; local installation does not publish it.
|
||||
|
||||
## Current token vocabulary
|
||||
|
||||
Each scheme is a map under `tokens.light` or `tokens.dark`. Both schemes must
|
||||
contain at least one valid token, or the pack will not load. Omitted tokens
|
||||
retain Studio defaults or derive from another declared surface.
|
||||
|
||||
The authoritative vocabulary is the kernel's [`Tokens` map](../internal/ext/theme/tokens.go), with value validation in `validToken`, `isColour`, `isLength` and `isFontStack` in that same file. Use those definitions when choosing token names and values; this guide does not maintain a second token table.
|
||||
|
||||
The frontend maps the vocabulary to CSS variables in [`theme.ts`](../desktop/frontend-next/src/ui/theme.ts). The kernel's [`TestThemeTokenVocabularyMatchesTheFrontend`](../internal/ext/theme/tokens_test.go) checks that both sides agree.
|
||||
|
||||
Status colours such as `ok`, `warn` and `err` belong to Studio and cannot be
|
||||
recoloured by a pack. `sidebar` and `chat` are not current token names.
|
||||
An unknown token or invalid value is dropped, while valid tokens still load;
|
||||
the theme choice shows the resulting warnings.
|
||||
|
||||
User contrast settings can adjust a theme's text colours. Check readability
|
||||
with the contrast settings and both colour schemes instead of assuming the
|
||||
JSON colour is always the final text colour.
|
||||
|
||||
The retired desktop's `baseStyle`, `recipes`, `taskBackground` and
|
||||
`schemaVersion: 2` are not the Studio authoring contract.
|
||||
[Theme Pack V2](THEME_PACK.md) remains the legacy reference.
|
||||
|
||||
## Optional local images
|
||||
|
||||
Keep image files next to `theme.json`. Studio recognises an optional
|
||||
`background.png`, `.jpg`, `.jpeg` or `.webp`, and likewise a `preview` image.
|
||||
|
||||
The reader selects those conventional filenames; a `background.image` field
|
||||
alone does not locate an arbitrary file.
|
||||
Keep each image at most 8 MiB so the asset endpoint can serve it.
|
||||
|
||||
The current `background` block controls `focusX`, `focusY`, `safeArea`
|
||||
(`left`, `center`, `right`), `homeOpacity`, `taskOpacity` and
|
||||
`overlayStrength`. Numeric placement and opacity values are bounded to 0–1.
|
||||
|
||||
Use a low task opacity so the image does not compete with a transcript.
|
||||
Check your image licence and avoid including workspace screenshots or secrets.
|
||||
|
||||
## Disable and clean up
|
||||
|
||||
```bash
|
||||
reasonix plugin disable paper-dawn-kit
|
||||
reasonix plugin enable paper-dawn-kit
|
||||
reasonix plugin remove paper-dawn-kit --yes
|
||||
```
|
||||
|
||||
Restart Studio after external CLI changes, then reopen Appearance settings.
|
||||
Disabling hides the contributed
|
||||
theme and retains the copied files; re-enabling restores the same identity.
|
||||
Removing a copied package deletes its installed copy. Removing a linked
|
||||
package retains the source working folder.
|
||||
|
||||
Studio retains the selected theme identity when its plugin becomes unavailable
|
||||
and renders its default appearance until the theme is available again. Choose
|
||||
the default theme in Appearance settings to clear that selection explicitly.
|
||||
Themes do not enter model prompts or add tools to a conversation.
|
||||
@@ -8,6 +8,9 @@ archives. What Reasonix Studio reads instead:
|
||||
- plugin themes are packs of that shape, listed as
|
||||
`plugin:<plugin>:<directory>` (see `docs/PLUGIN_PACKAGES.md`).
|
||||
|
||||
For the current format, a directly installable starter, and its management
|
||||
lifecycle, use the [Studio theme author guide](THEME_AUTHOR_GUIDE.md).
|
||||
|
||||
Native theme packs for the Reasonix desktop app. Packs are controlled skins:
|
||||
semantic color tokens, density/corner recipes, and optional local images for
|
||||
the home and task/workspace scenes. They **cannot** run CSS, JavaScript, fonts,
|
||||
|
||||
Vendored
+3
-18
@@ -3,43 +3,28 @@
|
||||
"id": "paper-dawn",
|
||||
"name": "Paper Dawn",
|
||||
"author": "Reasonix",
|
||||
"description": "Template theme — solid tokens only, no background image. Royalty-free starter.",
|
||||
"description": "Studio theme template with light and dark palettes, no background image. CC0-1.0 starter.",
|
||||
"license": "CC0-1.0",
|
||||
"baseStyle": "graphite",
|
||||
"tokens": {
|
||||
"light": {
|
||||
"bg": "#f7f4ef",
|
||||
"panel": "#ffffff",
|
||||
"sidebar": "#f3efe8",
|
||||
"chat": "#fbfaf7",
|
||||
"fg": "#1c1917",
|
||||
"fgDim": "#57534e",
|
||||
"fgFaint": "#a8a29e",
|
||||
"border": "#e7e5e4",
|
||||
"accent": "#c2410c",
|
||||
"accentFg": "#fff7ed",
|
||||
"ok": "#15803d",
|
||||
"warn": "#b45309",
|
||||
"err": "#b91c1c"
|
||||
"accentFg": "#fff7ed"
|
||||
},
|
||||
"dark": {
|
||||
"bg": "#0c0b0a",
|
||||
"panel": "#171412",
|
||||
"sidebar": "#141210",
|
||||
"chat": "#0c0b0a",
|
||||
"fg": "#f5f5f4",
|
||||
"fgDim": "#a8a29e",
|
||||
"fgFaint": "#78716c",
|
||||
"border": "#292524",
|
||||
"accent": "#fb923c",
|
||||
"accentFg": "#0c0b0a",
|
||||
"ok": "#4ade80",
|
||||
"warn": "#fbbf24",
|
||||
"err": "#f87171"
|
||||
"accentFg": "#0c0b0a"
|
||||
}
|
||||
},
|
||||
"recipes": {
|
||||
"density": "comfortable",
|
||||
"corners": "soft"
|
||||
}
|
||||
}
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"apiVersion": "reasonix.io/plugin/v2",
|
||||
"name": "paper-dawn-kit",
|
||||
"version": "1.0.0",
|
||||
"description": "A local Studio theme template with light and dark palettes.",
|
||||
"contributes": {
|
||||
"themes": ["paper-dawn/theme.json"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
package serve
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"reasonix/internal/base/testenv"
|
||||
"reasonix/internal/contract/config"
|
||||
"reasonix/internal/ext/installsource"
|
||||
"reasonix/internal/ext/pluginpkg"
|
||||
"reasonix/internal/ext/theme"
|
||||
)
|
||||
|
||||
func TestStudioThemeAuthorExampleLifecycle(t *testing.T) {
|
||||
home := testenv.TempDir(t)
|
||||
reasonixHome := filepath.Join(home, ".reasonix")
|
||||
t.Setenv("REASONIX_HOME", reasonixHome)
|
||||
t.Setenv("REASONIX_STATE_HOME", reasonixHome)
|
||||
source := filepath.Join(testenv.TempDir(t), "theme-example")
|
||||
if err := os.CopyFS(source, os.DirFS(filepath.Join("..", "..", "..", "docs", "themes"))); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
raw, err := os.ReadFile(filepath.Join(source, "paper-dawn", "theme.json"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var authored struct {
|
||||
Name string `json:"name"`
|
||||
Tokens map[string]map[string]string `json:"tokens"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &authored); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
installer := installsource.NewTool(installsource.Options{
|
||||
ProjectRoot: testenv.TempDir(t), HomeDir: home, RequireApprovedPlan: true,
|
||||
})
|
||||
type installResult struct {
|
||||
OK bool `json:"ok"`
|
||||
Status string `json:"status"`
|
||||
Applied bool `json:"applied"`
|
||||
PlanID string `json:"planId"`
|
||||
Actions []struct {
|
||||
Target string `json:"target"`
|
||||
ThemeCount int `json:"themeCount"`
|
||||
SkillCount int `json:"skillCount"`
|
||||
AgentCount int `json:"agentCount"`
|
||||
CommandCount int `json:"commandCount"`
|
||||
PromptCount int `json:"promptCount"`
|
||||
HookCount int `json:"hookCount"`
|
||||
ToolCount int `json:"toolCount"`
|
||||
Runtime any `json:"runtime"`
|
||||
} `json:"actions"`
|
||||
}
|
||||
run := func(args map[string]any) installResult {
|
||||
t.Helper()
|
||||
input, err := json.Marshal(args)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
output, err := installer.Execute(t.Context(), input)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var result installResult
|
||||
if err := json.Unmarshal([]byte(output), &result); err != nil {
|
||||
t.Fatalf("install response %q: %v", output, err)
|
||||
}
|
||||
if !result.OK {
|
||||
t.Fatalf("install response: %s", output)
|
||||
}
|
||||
return result
|
||||
}
|
||||
const id = "plugin:paper-dawn-kit:paper-dawn"
|
||||
srv := themeServer(t)
|
||||
check := func(stage string, present, active bool) {
|
||||
t.Helper()
|
||||
t.Run(stage, func(t *testing.T) {
|
||||
response, err := http.Get(srv.URL + "/themes")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
t.Fatalf("theme listing = %d", response.StatusCode)
|
||||
}
|
||||
var rows []themeView
|
||||
if err := json.NewDecoder(response.Body).Decode(&rows); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
found := false
|
||||
for _, row := range rows {
|
||||
if row.ID != id {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
if row.Name != authored.Name || row.Active != active || len(row.Warnings) != 0 || !reflect.DeepEqual(row.Tokens, authored.Tokens) {
|
||||
t.Fatalf("theme row = %+v, want active=%t and all authored tokens without warnings", row, active)
|
||||
}
|
||||
if row.Background != nil || row.Sky != nil || row.HasPreview {
|
||||
t.Fatalf("token-only example has image or sky: %+v", row)
|
||||
}
|
||||
}
|
||||
if found != present {
|
||||
t.Fatalf("theme present = %t, want %t", found, present)
|
||||
}
|
||||
if _, err := theme.Load(id); (err == nil) != present {
|
||||
t.Fatalf("theme load error = %v, want present=%t", err, present)
|
||||
}
|
||||
})
|
||||
}
|
||||
check("not-installed", false, false)
|
||||
preview := run(map[string]any{"source": source, "kind": "plugin", "apply": false})
|
||||
if preview.Status != "planned" || preview.Applied || preview.PlanID == "" || len(preview.Actions) != 1 {
|
||||
t.Fatalf("preview = %+v", preview)
|
||||
}
|
||||
action := preview.Actions[0]
|
||||
if action.ThemeCount != 1 || action.SkillCount != 0 || action.AgentCount != 0 || action.CommandCount != 0 || action.PromptCount != 0 || action.HookCount != 0 || action.ToolCount != 0 || action.Runtime != nil {
|
||||
t.Fatalf("pure-theme preview = %+v", action)
|
||||
}
|
||||
check("preview", false, false)
|
||||
installed := run(map[string]any{"source": source, "kind": "plugin", "apply": true, "planId": preview.PlanID})
|
||||
if installed.Status != "done" || !installed.Applied || len(installed.Actions) != 1 {
|
||||
t.Fatalf("installed = %+v", installed)
|
||||
}
|
||||
installedRoot := installed.Actions[0].Target
|
||||
if installedRoot == "" {
|
||||
t.Fatal("install has no copied target")
|
||||
}
|
||||
if err := os.RemoveAll(source); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
check("copied-source-removed", true, false)
|
||||
activate := func(themeID string, status int) {
|
||||
t.Helper()
|
||||
response := postJSON(t, srv.URL+"/themes", map[string]string{"id": themeID})
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != status {
|
||||
t.Fatalf("activate %q = %d, want %d", themeID, response.StatusCode, status)
|
||||
}
|
||||
}
|
||||
activate(id, http.StatusNoContent)
|
||||
check("active", true, true)
|
||||
if err := pluginpkg.SetEnabled(reasonixHome, "paper-dawn-kit", false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
check("disabled", false, false)
|
||||
activate(id, http.StatusUnprocessableEntity)
|
||||
if cfg, err := config.Load(); err != nil || cfg.Desktop.ThemePack != id {
|
||||
t.Fatalf("disabled selection was not retained: config=%+v err=%v", cfg, err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(installedRoot, "paper-dawn", "theme.json")); err != nil {
|
||||
t.Fatalf("disable removed copied content: %v", err)
|
||||
}
|
||||
if err := pluginpkg.SetEnabled(reasonixHome, "paper-dawn-kit", true); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
check("re-enabled-active", true, true)
|
||||
removed := run(map[string]any{"op": "uninstall", "name": "paper-dawn-kit", "kind": "plugin"})
|
||||
if removed.Status != "done" || !removed.Applied {
|
||||
t.Fatalf("uninstall = %+v", removed)
|
||||
}
|
||||
check("removed", false, false)
|
||||
if _, err := os.Stat(installedRoot); !os.IsNotExist(err) {
|
||||
t.Fatalf("uninstall retained copied content: %v", err)
|
||||
}
|
||||
if cfg, err := config.Load(); err != nil || cfg.Desktop.ThemePack != id {
|
||||
t.Fatalf("removed selection was not retained: config=%+v err=%v", cfg, err)
|
||||
}
|
||||
activate("", http.StatusNoContent)
|
||||
if cfg, err := config.Load(); err != nil || cfg.Desktop.ThemePack != "" {
|
||||
t.Fatalf("default appearance was not restored: config=%+v err=%v", cfg, err)
|
||||
}
|
||||
check("default", false, false)
|
||||
}
|
||||
Reference in new issue
Block a user