feat(studio): align branding with documentation site (#546)

* feat(studio): align branding with documentation site

* fix(studio): refine brand lockups

* fix(studio): simplify welcome branding

* docs(studio): add comprehensive visual guide

* docs: promote Studio to top-level navigation

* fix(studio): address review feedback
This commit is contained in:
jinliyl 2026-09-15 13:29:55 +08:00 • committed by GitHub
parent fd6337fe42
commit fe336da566
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
33 changed files with 982 additions and 269 deletions

View file

@ -15,6 +15,7 @@ on:
- 'integrations/claude_code/README.md'
- 'integrations/hermes_agent/README.md'
- 'reme_studio/README*.md'
- 'reme_studio/figures/**'
- 'reme_studio/public/og.jpg'
- 'integrations/dsh/README*.md'
- 'integrations/dsh/figures/**'

View file

@ -12,6 +12,7 @@ on:
- "README.md"
- "README_ZH.md"
- "reme_studio/README*.md"
- "reme_studio/figures/**"
- "reme_studio/public/og.jpg"
- "integrations/dsh/README*.md"
- "integrations/dsh/figures/**"

View file

@ -106,6 +106,7 @@ function nav(language: "zh" | "en"): DefaultTheme.NavItem[] {
return [
{ text: zh ? "首页" : "Home", link: `/${language}/` },
{ text: zh ? "文档" : "Docs", link: `/${language}/quick_start` },
{ text: "Studio", link: `/${language}/workspace/studio` },
{ text: zh ? "集成" : "Integrations", link: `/${language}/integrations` },
{ text: zh ? "插件" : "Plugins", link: `/${language}/plugin_management` },
{ text: zh ? "评测" : "Benchmarks", link: `/${language}/benchmarks/longmemeval` },
@ -185,25 +186,18 @@ function integrationsSidebar(language: "zh" | "en"): DefaultTheme.SidebarItem[]
function pluginsSidebar(language: "zh" | "en"): DefaultTheme.SidebarItem[] {
const zh = language === "zh";
return [
{
text: zh ? "工作区" : "Workspace",
collapsed: false,
items: [{ text: "ReMe Studio", link: `/${language}/workspace/studio` }],
},
{
text: zh ? "插件" : "Plugins",
collapsed: false,
items: [
{ text: zh ? "插件管理" : "Plugin Management", link: `/${language}/plugin_management` },
{ text: zh ? "插件开发" : "Plugin Development", link: `/${language}/plugin_development` },
{ text: zh ? "每日论文" : "Daily Paper", link: `/${language}/plugins/daily-paper` },
{ text: "Auto Fin", link: `/${language}/plugins/auto-fin` },
{ text: "LME", link: `/${language}/plugins/lme` },
{ text: "BEAM", link: `/${language}/plugins/beam` },
],
},
];
return [{
text: zh ? "插件" : "Plugins",
collapsed: false,
items: [
{ text: zh ? "插件管理" : "Plugin Management", link: `/${language}/plugin_management` },
{ text: zh ? "插件开发" : "Plugin Development", link: `/${language}/plugin_development` },
{ text: zh ? "每日论文" : "Daily Paper", link: `/${language}/plugins/daily-paper` },
{ text: "Auto Fin", link: `/${language}/plugins/auto-fin` },
{ text: "LME", link: `/${language}/plugins/lme` },
{ text: "BEAM", link: `/${language}/plugins/beam` },
],
}];
}
function benchmarksSidebar(language: "zh" | "en"): DefaultTheme.SidebarItem[] {
@ -235,7 +229,7 @@ function singlePageSidebar(language: "zh" | "en", page: "blog" | "faq"): Default
function sidebars(language: "zh" | "en"): DefaultTheme.SidebarMulti {
return {
[`/${language}/integrations`]: integrationsSidebar(language),
[`/${language}/workspace/`]: pluginsSidebar(language),
[`/${language}/workspace/`]: [],
[`/${language}/plugins/`]: pluginsSidebar(language),
[`/${language}/plugin_management`]: pluginsSidebar(language),
[`/${language}/plugin_development`]: pluginsSidebar(language),

View file

@ -64,6 +64,18 @@ const externalDocumentRewrites = {
"integrations/openclaw/README_ZH.md": [
["(./README.md)", "(/en/integrations/openclaw)"],
],
"reme_studio/README.md": [
["(./README_ZH.md)", "(/zh/workspace/studio)"],
["(./figures/", "(/figures/studio/"],
["(https://github.com/agentscope-ai/ReMe/blob/main/reme_studio/README_ZH.md)", "(/zh/workspace/studio)"],
["(https://raw.githubusercontent.com/agentscope-ai/ReMe/main/reme_studio/figures/", "(/figures/studio/"],
],
"reme_studio/README_ZH.md": [
["(./README.md)", "(/en/workspace/studio)"],
["(./figures/", "(/figures/studio/"],
["(https://github.com/agentscope-ai/ReMe/blob/main/reme_studio/README.md)", "(/en/workspace/studio)"],
["(https://raw.githubusercontent.com/agentscope-ai/ReMe/main/reme_studio/figures/", "(/figures/studio/"],
],
};
const externalDocumentPreambles = {
@ -220,6 +232,9 @@ for (const [destination, source] of externalDocuments) {
await cp(path.join(repoDir, "integrations/dsh/figures"), path.join(outputDir, "public/figures/dsh"), {
recursive: true,
});
await cp(path.join(repoDir, "reme_studio/figures"), path.join(outputDir, "public/figures/studio"), {
recursive: true,
});
for (const language of ["zh", "en"]) {
await cp(

View file

@ -49,6 +49,8 @@ const requiredFiles = [
"en/configuration.html",
"zh/services.html",
"en/services.html",
"zh/workspace/studio.html",
"en/workspace/studio.html",
"zh/reference/jobs.html",
"en/reference/jobs.html",
"zh/configuration/llms.txt",
@ -70,6 +72,11 @@ assert.match(ChineseHomepage, /用真实评测/);
assert.match(ChineseHomepage, /89\.4%/);
assert.match(ChineseHomepage, /公开、透明的访问趋势/);
const ChineseStudio = await readFile(path.join(outputDir, "zh/workspace/studio.html"), "utf8");
assert.match(ChineseStudio, /class="VPLink link VPNavBarMenuLink active" href="\/zh\/workspace\/studio"/);
assert.match(ChineseStudio, /<span[^>]*>Studio<\/span>/);
assert.doesNotMatch(ChineseStudio, /<aside[^>]*class="VPSidebar/);
const ChineseTraffic = await readFile(path.join(outputDir, "zh/traffic.html"), "utf8");
assert.match(ChineseTraffic, /S1OZK1PSDLEpyiU5/);

View file

@ -34,6 +34,7 @@ test("generates every required bilingual guide", async () => {
await access(path.join(generatedDir, "zh/integrations/openclaw.md"));
await access(path.join(generatedDir, "en/integrations/openclaw.md"));
await access(path.join(generatedDir, "public/figures/dsh/reme-status-overview.png"));
await access(path.join(generatedDir, "public/figures/studio/studio-overview.png"));
});
test("maps mirrored pages back to their canonical repository sources", async () => {
@ -69,6 +70,26 @@ test("publishes portable and accurate DSH instructions", async () => {
assert.match(chinese, /每 5 秒仅刷新 DSH 插件的运行时计数/);
});
test("publishes Studio screenshots with site-safe links", async () => {
const english = await readFile(path.join(generatedDir, "en/workspace/studio.md"), "utf8");
const chinese = await readFile(path.join(generatedDir, "zh/workspace/studio.md"), "utf8");
assert.match(english, /\(\/figures\/studio\/studio-overview\.png\)/);
assert.match(chinese, /\(\/figures\/studio\/settings-status\.png\)/);
assert.match(english, /\(\/zh\/workspace\/studio\)/);
assert.match(chinese, /\(\/en\/workspace\/studio\)/);
});
test("keeps Studio source READMEs portable for package registries", async () => {
for (const name of ["README.md", "README_ZH.md"]) {
const source = await readFile(path.join(repoDir, "reme_studio", name), "utf8");
assert.doesNotMatch(source, /\]\(\.\/figures\//);
assert.match(
source,
/\]\(https:\/\/raw\.githubusercontent\.com\/agentscope-ai\/ReMe\/main\/reme_studio\/figures\//,
);
}
});
test("generates the callable Job reference from default.yaml", async () => {
const config = parseYaml(await readFile(path.join(repoDir, "reme/config/default.yaml"), "utf8"));
const callableJobs = Object.entries(config.jobs)
@ -114,6 +135,8 @@ test("tracks every generated input in documentation CI and deployment", async ()
"integrations/dsh/README*.md",
"integrations/dsh/figures/**",
"integrations/openclaw/README*.md",
"reme_studio/README*.md",
"reme_studio/figures/**",
"benchmark/toolmemory/gitcha.png",
];
for (const workflow of ["ci-docs.yml", "deploy-docs.yml"]) {

View file

@ -2,13 +2,60 @@
English | [简体中文](https://github.com/agentscope-ai/ReMe/blob/main/reme_studio/README_ZH.md)
ReMe Studio is the local web workspace for ReMe. It lets you browse and edit user-owned workspace files, explore memory
links, and chat with the ReMe Agent without moving durable memory into a separate application database. Search indexes,
graphs, and other derived metadata remain rebuildable from the source files.
ReMe Studio is the local web workspace for [ReMe](https://github.com/agentscope-ai/ReMe). It provides one place to
browse and edit user-owned memory files, inspect the relationships between them, talk to the ReMe Agent, and understand
the health of the local service.
![ReMe Studio workspace](https://github.com/user-attachments/assets/7d0db0d4-69c5-49ef-b1ca-ef8c6cab1138)
Studio follows ReMe's local-first model: Markdown and other workspace files remain the durable source of truth. Search
indexes, catalogs, graphs, caches, and runtime metadata stay derived and rebuildable.
## Installation
![ReMe Studio overview](https://raw.githubusercontent.com/agentscope-ai/ReMe/main/reme_studio/figures/studio-overview.png)
> All screenshots in this README were captured from a real local ReMe service with the Studio UI set to English. The
> isolated `Project Aurora` workspace is fictional and contains no user data, credentials, or private endpoints.
## What Studio provides
| Area | Capabilities |
| --------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| Workspace | Browse the complete workspace, focused daily notes, or durable knowledge; refresh automatically as files change |
| Markdown | Preview front matter and GitHub Flavored Markdown, edit with Monaco, save safely, download, and keep multiple tabs open |
| Memory graph | Explore indexed wikilinks by knowledge category, follow link direction, zoom, fit, and open source notes |
| Agent chat | Stream answers, reasoning, tool activity, approvals, data blocks, and usage; reference workspace files by dragging them into chat |
| Service center | Inspect process and component health, rebuild derived indexes, review the redacted effective configuration, and verify versions |
| Personalization | Switch between English and Chinese and select light, dark, or system appearance |
## How it fits together
```text
Your local workspace files
│
▼
ReMe HTTP service
├── file operations
├── derived indexes
├── wikilink graph
└── read-only Agent chat
│
▼
ReMe Studio
```
Studio is a client of the ReMe HTTP service. It does not replace the workspace with a separate document database and it
does not make browser-local state the durable source of truth. Tabs and interface preferences are conveniences; the
files in the configured ReMe workspace remain authoritative.
## Requirements
- Python 3.11 or newer with ReMe installed.
- A running ReMe HTTP service.
- A working Agent and model configuration only if Agent chat is used.
- Node.js 22.13 or newer only when developing or building Studio from source.
See the [ReMe repository README](https://github.com/agentscope-ai/ReMe#readme) for backend installation, workspace, and
model configuration.
## Install
Install Studio together with ReMe's optional integrations:
@ -16,40 +63,221 @@ Install Studio together with ReMe's optional integrations:
pip install "reme-ai[core]"
```
For Studio without the other optional integrations, use `pip install "reme-ai[web]"`. The base `reme-ai` package is
headless and does not include the frontend assets.
Install only the web extra when the other optional integrations are unnecessary:
The same prebuilt static workspace is available for Node.js applications:
```bash
pip install "reme-ai[web]"
```
The base `reme-ai` package is headless and does not include the frontend assets.
Node.js applications can install the same prebuilt static workspace:
```bash
npm install @agentscope-ai/reme_studio
```
Its static entry point is installed at `@agentscope-ai/reme_studio/dist-static/index.html`.
The static entry point is installed at `@agentscope-ai/reme_studio/dist-static/index.html`.
## Features
## Start Studio
- **Workspace browsing**: browse the full workspace or focus on journal and knowledge files through dedicated views. The
navigator refreshes as files change on disk.
- **Markdown editing and preview**: open multiple files in tabs, render Markdown front matter and GitHub Flavored
Markdown, edit with Monaco, save with optimistic modification-time checks, and download files locally.
- **Memory graph**: inspect indexed wikilinks under the `wiki`, `personal`, and
`procedure` knowledge roots, follow inbound and outbound links, and open the corresponding Markdown source.
- **Agent chat**: stream conversations with the read-only workspace Agent, see tool calls and token usage, and drag
workspace files into the conversation as references.
- **Service management**: inspect service and component memory usage, review the effective redacted configuration and
version, and rebuild derived indexes without modifying source memory files.
- **Personalization**: switch between English and Chinese, and use light, dark, or system appearance.
Start the ReMe HTTP service:
## Requirements
```bash
reme start
```
- Python 3.11 or newer with ReMe installed.
- A running ReMe HTTP service. Agent chat additionally requires a working Agent and model configuration.
- Node.js 22.13 or newer is required only when developing or building Studio from source.
Then open <http://127.0.0.1:2333>. The default HTTP service serves both the API and the packaged Studio frontend.
See the [repository README](https://github.com/agentscope-ai/ReMe#readme) for ReMe installation and backend configuration.
To use a different workspace or port, pass normal ReMe configuration overrides:
## Development
```bash
reme start workspace_dir=/absolute/path/to/workspace service.port=8000
```
Studio displays the connected endpoint at the bottom of the navigator. A green indicator means the browser can reach
the service. If Studio is hosted separately, configure its API URL as described in
[Frontend configuration](#frontend-configuration).
## Interface tour
### 1. Workspace navigator
The left navigator exposes four primary entry points:
- **Files** shows supported files across the entire ReMe workspace.
- **Daily** focuses on files under the configured daily-memory directory.
- **Knowledge** focuses on the `wiki`, `personal`, and `procedure` directories under durable digest memory.
- **Chat** opens a fresh Agent conversation without closing files or graphs that are already open.
Directories can be expanded independently. The navigator hides dotfiles and dot-directories, orders the newest files
first, applies a bounded result limit, and refreshes when files change on disk. The divider can be dragged to resize the
navigator, and the menu control in the top bar can collapse it.
![Workspace navigator and Markdown preview](https://raw.githubusercontent.com/agentscope-ai/ReMe/main/reme_studio/figures/files-workspace.png)
The tab strip keeps files, graphs, and conversations together. Closing a tab does not delete its file. When an edited
file has unsaved changes, Studio protects it from an accidental close and also supports closing the current tab or the
other tabs from the tab context menu.
### 2. Daily notes
The Daily view gathers both day-index pages and dated note directories without changing their on-disk paths. It is a
focused view of the same local workspace, so a note opened here can also appear in Files and remains available to ReMe's
indexing and Agent workflows.
![Daily notes](https://raw.githubusercontent.com/agentscope-ai/ReMe/main/reme_studio/figures/daily-notes.png)
Use Daily to review chronological memory, open generated resource notes, and move between recent work without browsing
the rest of the workspace tree.
### 3. Markdown preview
Preview mode renders:
- YAML front matter in a dedicated summary block.
- Headings, lists, links, code, tables, task lists, and other GitHub Flavored Markdown.
- The original workspace-relative path above the document.
The download control saves a local copy through the browser. Preview mode never modifies the source file.
![Markdown preview](https://raw.githubusercontent.com/agentscope-ai/ReMe/main/reme_studio/figures/files-workspace.png)
### 4. Markdown editing and safe saves
Edit mode uses Monaco with Markdown syntax highlighting, line numbers, keyboard navigation, and a full-height editing
surface. Switch between **Preview** and **Edit** at any time.
![Markdown editor](https://raw.githubusercontent.com/agentscope-ai/ReMe/main/reme_studio/figures/markdown-editor.png)
Studio records the file modification time when it loads a document and sends that value back on save. If another
process changed the file in the meantime, the ReMe service rejects the stale write instead of silently overwriting the
newer content. After a successful save, Studio refreshes the file state and clears the unsaved marker.
### 5. Knowledge and memory graphs
Knowledge groups durable memory into three conventional roots:
- `wiki` for entities, projects, topics, and stable knowledge.
- `personal` for user-specific preferences and long-lived context.
- `procedure` for repeatable workflows and operating knowledge.
Each category has a **Graph** action. The graph is generated from ReMe's indexed wikilinks and preserves their original
source-to-target direction.
![Memory graph](https://raw.githubusercontent.com/agentscope-ai/ReMe/main/reme_studio/figures/memory-graph.png)
The graph view provides:
- Indexed-file nodes, category roots, and directed wikilink edges.
- Node and link counts for the selected category.
- Zoom in, zoom out, and fit-to-view controls.
- A legend for file nodes and link direction.
- Direct opening of the corresponding Markdown source when a file node is selected.
The graph is derived state. If it is empty, first allow ReMe to ingest the relevant files; rebuilding search indexes in
Settings does not rescan files or reconstruct the wikilink graph.
### 6. Agent chat
Chat opens in the same tab workspace, so files and graphs can remain available while a conversation is in progress.
![Agent chat](https://raw.githubusercontent.com/agentscope-ai/ReMe/main/reme_studio/figures/agent-chat.png)
The conversation surface supports:
- Token-by-token streaming from the ReMe Agent.
- Separate rendering for answer text, reasoning, tool calls, structured data, approvals, and usage information.
- Collapsible completed reasoning and tool blocks to keep long conversations readable.
- Multiple independent chat tabs and resumable backend session identifiers.
- Starter prompts for common memory questions.
- Dragging a workspace file into the composer to insert a path reference.
- `Enter` to send and `Shift+Enter` for a new line.
The built-in chat job is read-only with respect to the workspace. It can search and read context but should not be used
as a substitute for explicitly saving durable knowledge. Chat requires a valid Agent/model configuration; file browsing,
editing, graphs, and service inspection do not.
### 7. Service status
Open **Settings** from the top bar to inspect the backend that Studio is actually using.
![Service status](https://raw.githubusercontent.com/agentscope-ai/ReMe/main/reme_studio/figures/settings-status.png)
Status reports:
- Whether the ReMe service is reachable and the endpoint in use.
- The current ReMe backend version.
- Process resident memory and total memory estimated for stateful components.
- The number of healthy components.
- Runtime facts for the file graph, file store, keyword index, and embedding store when configured, including nodes,
edges, chunks, documents, vocabulary, dimensions, cache size, and component memory where available.
Use **Refresh** to request a new snapshot after files are indexed or service configuration changes.
### 8. Index management
The Index page provides an explicit maintenance action for derived search state.
![Index management](https://raw.githubusercontent.com/agentscope-ai/ReMe/main/reme_studio/figures/settings-index.png)
**Rebuild index** reconstructs the BM25, embedding, and tag indexes from chunks that ReMe has already ingested. Before
running, Studio asks for confirmation. This action does not scan the workspace, rechunk files, edit source memory, or
rebuild the wikilink graph. Those boundaries preserve ReMe's file-native source of truth.
### 9. Effective configuration
Configuration shows the fully resolved application configuration returned by the connected backend.
![Effective configuration](https://raw.githubusercontent.com/agentscope-ai/ReMe/main/reme_studio/figures/settings-configuration.png)
This is useful for confirming the active workspace, directory layout, service options, jobs, and component backends
after file configuration and command-line overrides have been merged. Sensitive fields are redacted by ReMe before the
configuration reaches the browser.
### 10. Version and endpoint
Version presents the ReMe backend version and service endpoint separately from the Studio version shown in the top-left
brand lockup.
![Version and endpoint](https://raw.githubusercontent.com/agentscope-ai/ReMe/main/reme_studio/figures/settings-version.png)
This distinction helps when diagnosing a frontend/backend mismatch: the top bar identifies the installed Studio build,
while Settings identifies the connected ReMe service.
### 11. Language and appearance
The top bar switches the entire interface between English and Chinese. Appearance supports **Light**, **Dark**, and
**System**; System follows the operating-system color preference.
![ReMe Studio dark appearance](https://raw.githubusercontent.com/agentscope-ai/ReMe/main/reme_studio/figures/appearance-dark.png)
The selected language and appearance are remembered in the browser. Both themes use the same ReMe documentation-site
brand palette and retain health, selection, focus, and graph contrast.
## Frontend configuration
Copy `.env.example` to `.env.local` when persistent local overrides are useful. The vinext/Sites build reads
`NEXT_PUBLIC_*`; the FastAPI/static build reads the matching `VITE_*` names.
| Setting | Build | Purpose |
| --------------------------------------- | ------------ | -------------------------------------------------------------- |
| `NEXT_PUBLIC_REME_API_URL` | vinext/Sites | ReMe HTTP service URL; defaults to `http://127.0.0.1:2333` |
| `NEXT_PUBLIC_REME_WORKSPACE_EXTENSIONS` | vinext/Sites | Comma-separated file extensions shown in the workspace |
| `VITE_REME_API_URL` | static | ReMe HTTP service URL; use `/` for same-origin FastAPI hosting |
| `VITE_REME_WORKSPACE_EXTENSIONS` | static | Static-build counterpart of the workspace extension list |
Markdown and text files are visible by default. To include additional text formats:
```bash
NEXT_PUBLIC_REME_WORKSPACE_EXTENSIONS=md,txt,mdx
VITE_REME_WORKSPACE_EXTENSIONS=md,txt,mdx
```
Only enable formats the browser can safely treat as text. ReMe still enforces workspace containment, allowed paths,
encoding checks, size limits, and optimistic modification-time checks on its API boundary.
## Develop from source
Start ReMe from the repository root, then run the frontend in another terminal:
@ -63,14 +291,14 @@ npm install
npm run dev
```
Open <http://localhost:3000>. The frontend connects to
`http://127.0.0.1:2333` by default. Override it when needed:
Open <http://localhost:3000>. The development frontend connects to `http://127.0.0.1:2333` by default. Override it when
needed:
```bash
NEXT_PUBLIC_REME_API_URL=http://127.0.0.1:8000 npm run dev
```
## ReMe-hosted static build
## Build the ReMe-hosted static frontend
ReMe can serve Studio from the same FastAPI process as its HTTP API. Build the static variant and restart ReMe:
@ -82,36 +310,52 @@ cd ..
reme start
```
Open <http://127.0.0.1:2333>. The static build uses same-origin requests by default. For standalone static development,
run `npm run dev:static` and set
`VITE_REME_API_URL` to the running ReMe service URL when necessary.
The regular `npm run build` command remains the vinext/Sites deployment build;
`npm run build:static` creates `dist-static/` exclusively for FastAPI and Python package distribution.
## Configuration
Copy `.env.example` to `.env.local` when persistent local overrides are useful. The vinext/Sites build reads
`NEXT_PUBLIC_*`; the FastAPI/static build reads the matching `VITE_*` names:
| Setting | Build | Purpose |
| --------------------------------------- | ------------ | -------------------------------------------------------------- |
| `NEXT_PUBLIC_REME_API_URL` | vinext/Sites | ReMe HTTP service URL; defaults to `http://127.0.0.1:2333` |
| `NEXT_PUBLIC_REME_WORKSPACE_EXTENSIONS` | vinext/Sites | Comma-separated file extensions visible in the workspace |
| `VITE_REME_API_URL` | static | ReMe HTTP service URL; use `/` for same-origin FastAPI hosting |
| `VITE_REME_WORKSPACE_EXTENSIONS` | static | Static-build counterpart of the workspace extension list |
The workspace hides dotfiles and dot-directories. It displays Markdown and text files by default. For example:
Open <http://127.0.0.1:2333>. The static build uses same-origin requests by default. For standalone static development:
```bash
NEXT_PUBLIC_REME_WORKSPACE_EXTENSIONS=md,txt,mdx
VITE_REME_WORKSPACE_EXTENSIONS=md,txt,mdx
VITE_REME_API_URL=http://127.0.0.1:2333 npm run dev:static
```
The memory graph requires an index built by ReMe. Rebuilding the index from the Studio settings regenerates derived data
from workspace files and does not modify the source memory.
`npm run build` remains the vinext/Sites deployment build. `npm run build:static` creates `dist-static/` exclusively for
FastAPI and Python/npm package distribution. Change frontend source rather than committing generated distribution files.
## Checks
## Troubleshooting
### Studio says the service is unavailable
- Confirm `reme start` is still running.
- Check the endpoint displayed at the bottom of the navigator.
- For a separately hosted frontend, set the correct `NEXT_PUBLIC_REME_API_URL` or `VITE_REME_API_URL` before starting or
building it.
- Open Settings → Status and use Refresh after correcting the endpoint.
### Files are missing from the navigator
- Confirm ReMe was started with the intended `workspace_dir`.
- Remember that dotfiles and dot-directories are intentionally hidden.
- Check `*_REME_WORKSPACE_EXTENSIONS` when the file is not Markdown or plain text.
- Use Files instead of the focused Daily or Knowledge view.
### The graph is empty or incomplete
- Confirm the notes are under `digest/wiki`, `digest/personal`, or `digest/procedure`.
- Confirm the source files contain wikilinks and have been ingested by ReMe.
- Do not expect Settings → Rebuild index to rescan files or rebuild the graph; it only rebuilds search indexes from
existing chunks.
### A Markdown save is rejected
The file changed after Studio loaded it. Preserve the newer on-disk version, reopen or refresh the file, reapply the
intended edit, and save again. This conflict is deliberate protection against silent data loss.
### Chat does not start
File features can work while chat is unavailable. Verify the configured Agent wrapper, model, API key, and provider
endpoint in the ReMe backend, then check Settings → Status. Do not place credentials in frontend environment variables.
## Validation
Run proportionate checks from `reme_studio/`:
```bash
npm run format:check
@ -120,3 +364,6 @@ npm run build
npm run build:static
npm test
```
The screenshots above also serve as a visual tour of the supported interface, but automated checks remain the source of
truth for frontend behavior.

View file

@ -2,21 +2,67 @@
[English](https://github.com/agentscope-ai/ReMe/blob/main/reme_studio/README.md) | 简体中文
ReMe Studio 是 ReMe 的本地 Web 工作区。你可以在这里浏览和编辑自己拥有的工作区文件、探索记忆之间的联系,并与 ReMe Agent
对话,而无需将持久记忆迁移到独立的应用数据库中。搜索索引、图谱和其他派生元数据均可根据源文件重建。
ReMe Studio 是 [ReMe](https://github.com/agentscope-ai/ReMe) 的本地 Web 工作区。你可以在一个界面中浏览和编辑用户拥有的记忆文件、查看文件之间的关系、与 ReMe Agent 对话,并了解本地服务的运行状态。
![ReMe Studio 工作区](https://github.com/user-attachments/assets/7d0db0d4-69c5-49ef-b1ca-ef8c6cab1138)
Studio 遵循 ReMe 的本地优先理念:Markdown 和其他工作区文件始终是持久数据的唯一事实来源;搜索索引、目录、图谱、缓存和运行时元数据都是可重建的派生数据。
![ReMe Studio 概览](https://raw.githubusercontent.com/agentscope-ai/ReMe/main/reme_studio/figures/studio-overview.png)
> 本 README 中的所有截图均来自真实运行的本地 ReMe 服务,Studio 界面统一设置为英文。截图使用隔离的虚构 `Project Aurora` 工作区,不包含用户数据、凭证或私有服务地址。
## Studio 提供什么
| 区域 | 能力 |
| ---------- | ----------------------------------------------------------------------------------------------------- |
| 工作区 | 浏览完整工作区、聚焦日记或长期知识;磁盘文件变化后自动刷新 |
| Markdown | 预览 front matter 和 GitHub Flavored Markdown,使用 Monaco 编辑、安全保存、下载,并同时打开多个标签页 |
| 记忆图谱 | 按知识分类查看已索引的 wikilink、识别链接方向、缩放和适应画布,并打开源笔记 |
| Agent 对话 | 流式展示回答、推理、工具、审批、结构化数据和用量;可把工作区文件拖入对话作为引用 |
| 服务中心 | 查看进程和组件健康状态、重建派生索引、检查脱敏后的生效配置,并核对版本 |
| 个性化 | 切换中英文,以及浅色、深色或跟随系统的外观 |
## 工作方式
```text
用户拥有的本地工作区文件
│
▼
ReMe HTTP 服务
├── 文件操作
├── 派生索引
├── wikilink 图谱
└── 只读 Agent 对话
│
▼
ReMe Studio
```
Studio 是 ReMe HTTP 服务的客户端。它不会用独立文档数据库替代工作区,也不会把浏览器中的状态当作持久数据来源。标签页和界面偏好只是使用便利;配置的 ReMe 工作区中的文件始终具有权威性。
## 环境要求
- Python 3.11 或更高版本,并已安装 ReMe。
- 正在运行的 ReMe HTTP 服务。
- 仅在使用 Agent 对话时需要可用的 Agent 和模型配置。
- 仅在从源码开发或构建 Studio 时需要 Node.js 22.13 或更高版本。
后端安装、工作区和模型配置请参阅 [ReMe 仓库中文 README](https://github.com/agentscope-ai/ReMe/blob/main/README_ZH.md)。
## 安装
安装 Studio 和 ReMe 的可选集成功能:
安装 Studio 及 ReMe 的可选集成功能:
```bash
pip install "reme-ai[core]"
```
如果只需要 Studio,不需要其他可选集成,可以使用 `pip install "reme-ai[web]"`。基础 `reme-ai` 包以无界面模式分发,
不包含前端资源。
如果不需要其他可选集成,只安装 Web extra:
```bash
pip install "reme-ai[web]"
```
基础 `reme-ai` 包以无界面模式分发,不包含前端资源。
Node.js 应用也可以安装同一份预构建静态工作区:
@ -26,26 +72,178 @@ npm install @agentscope-ai/reme_studio
静态入口安装在 `@agentscope-ai/reme_studio/dist-static/index.html`。
## 功能
## 启动 Studio
- **浏览工作区**:浏览完整工作区,或通过独立视图聚焦日记和知识文件;磁盘中的文件发生变化后,导航器会自动刷新。
- **Markdown 编辑与预览**:在多个标签页中打开文件,渲染 Markdown front matter 和 GitHub Flavored Markdown,使用 Monaco
编辑器编辑,通过修改时间检查安全保存,并可将文件下载到本地。
- **记忆图谱**:查看知识库 `wiki`、`personal` 和 `procedure` 目录中已索引的 wikilink,检查入链和出链,并从图谱打开对应的
Markdown 源文件。
- **Agent 对话**:与只读工作区 Agent 进行流式对话,查看工具调用和 token 用量,还可将工作区文件拖入对话作为引用。
- **服务管理**:查看服务及组件的内存使用情况、当前生效的脱敏配置和版本,并在不修改记忆源文件的情况下重建派生索引。
- **个性化设置**:切换中英文界面,并使用浅色、深色或跟随系统的外观。
启动 ReMe HTTP 服务:
## 环境要求
```bash
reme start
```
- Python 3.11 或更高版本,并已安装 ReMe。
- 正在运行的 ReMe HTTP 服务。Agent 对话还需要可用的 Agent 和模型配置。
- 只有从源码开发或构建 Studio 时才需要 Node.js 22.13 或更高版本。
然后打开 <http://127.0.0.1:2333>。默认 HTTP 服务会同时提供 API 和打包后的 Studio 前端。
ReMe 的安装和后端配置请参阅[仓库中文 README](https://github.com/agentscope-ai/ReMe/blob/main/README_ZH.md)。
如需使用其他工作区或端口,可以传入正常的 ReMe 配置覆盖:
## 本地开发
```bash
reme start workspace_dir=/absolute/path/to/workspace service.port=8000
```
Studio 会在导航器底部显示当前连接的服务地址;绿色指示点表示浏览器能够访问该服务。如果 Studio 与后端分开托管,请按[前端配置](#前端配置)设置 API 地址。
## 界面与能力详解
### 1. 工作区导航器
左侧导航器提供四个主要入口:
- **Files** 显示整个 ReMe 工作区中受支持的文件。
- **Daily** 聚焦配置的日记目录。
- **Knowledge** 聚焦长期摘要记忆下的 `wiki`、`personal` 和 `procedure` 目录。
- **Chat** 新建 Agent 对话,同时保留已经打开的文件和图谱。
目录可以独立展开。导航器会隐藏点文件和点目录,优先显示最新文件,应用有界的结果数量,并在磁盘文件变化后刷新。可以拖动分隔线调整导航器宽度,也可以使用顶部菜单按钮将其收起。
![工作区导航器与 Markdown 预览](https://raw.githubusercontent.com/agentscope-ai/ReMe/main/reme_studio/figures/files-workspace.png)
标签栏会把文件、图谱和对话放在同一个工作空间中。关闭标签页不会删除文件。文件存在未保存修改时,Studio 会阻止误关闭;标签页上下文菜单还支持关闭当前标签页或关闭其他标签页。
### 2. 日记
Daily 视图会汇总日期索引页面和按日期组织的笔记目录,但不会改变它们在磁盘上的路径。它只是同一本地工作区的聚焦视图,因此在这里打开的笔记也可以出现在 Files 中,并继续参与 ReMe 的索引和 Agent 工作流。
![日记视图](https://raw.githubusercontent.com/agentscope-ai/ReMe/main/reme_studio/figures/daily-notes.png)
可以用 Daily 检查时间顺序记忆、打开资源生成的笔记,并在无需浏览完整工作区树的情况下查看近期工作。
### 3. Markdown 预览
预览模式支持:
- 在独立摘要区展示 YAML front matter。
- 渲染标题、列表、链接、代码、表格、任务列表和其他 GitHub Flavored Markdown。
- 在文档上方显示原始的工作区相对路径。
下载按钮会通过浏览器保存文件副本;预览模式不会修改源文件。
![Markdown 预览](https://raw.githubusercontent.com/agentscope-ai/ReMe/main/reme_studio/figures/files-workspace.png)
### 4. Markdown 编辑与安全保存
编辑模式使用 Monaco,提供 Markdown 语法高亮、行号、键盘导航和占满可用高度的编辑区域。可以随时在 **Preview** 和 **Edit** 之间切换。
![Markdown 编辑器](https://raw.githubusercontent.com/agentscope-ai/ReMe/main/reme_studio/figures/markdown-editor.png)
Studio 会在加载文档时记录文件修改时间,并在保存时把该值发送给后端。如果其他进程已经修改文件,ReMe 服务会拒绝过期写入,而不是静默覆盖更新后的内容。保存成功后,Studio 会刷新文件状态并清除未保存标记。
### 5. 知识与记忆图谱
Knowledge 将长期记忆分为三个约定目录:
- `wiki`:实体、项目、主题和稳定知识。
- `personal`:用户偏好和长期个人上下文。
- `procedure`:可重复执行的流程和操作知识。
每个分类都有 **Graph** 操作。图谱由 ReMe 已索引的 wikilink 生成,并保留原始的源到目标方向。
![记忆图谱](https://raw.githubusercontent.com/agentscope-ai/ReMe/main/reme_studio/figures/memory-graph.png)
图谱视图提供:
- 已索引文件节点、分类根节点和有向 wikilink 边。
- 当前分类的节点数与链接数。
- 放大、缩小和适应画布控制。
- 文件节点和链接方向图例。
- 选择文件节点后直接打开相应 Markdown 源文件。
图谱属于派生状态。如果图谱为空,请先让 ReMe 摄取相关文件;Settings 中的搜索索引重建不会重新扫描文件,也不会重新构建 wikilink 图谱。
### 6. Agent 对话
Chat 会在同一个标签工作区中打开,因此对话进行时仍可保留文件和图谱。
![Agent 对话](https://raw.githubusercontent.com/agentscope-ai/ReMe/main/reme_studio/figures/agent-chat.png)
对话界面支持:
- ReMe Agent 逐 token 流式输出。
- 分别展示回答文本、推理、工具调用、结构化数据、审批和用量信息。
- 自动折叠已经完成的推理和工具块,让长对话更易阅读。
- 多个独立对话标签页和可恢复的后端 session id。
- 常见记忆问题的起始提示。
- 将工作区文件拖入输入框以插入路径引用。
- 按 `Enter` 发送,按 `Shift+Enter` 换行。
内置 chat job 对工作区只读:它可以搜索和读取上下文,但不应替代明确保存长期知识的操作。Agent 对话需要有效的 Agent/模型配置;文件浏览、编辑、图谱和服务检查不需要模型。
### 7. 服务状态
从顶部栏打开 **Settings**,可以检查 Studio 实际连接的后端。
![服务状态](https://raw.githubusercontent.com/agentscope-ai/ReMe/main/reme_studio/figures/settings-status.png)
Status 会显示:
- ReMe 服务是否可达及当前使用的服务地址。
- 当前 ReMe 后端版本。
- 进程常驻内存及有状态组件估算的总内存。
- 健康组件数量。
- 已配置的文件图谱、文件存储、关键词索引和 embedding 存储的运行信息,包括节点、边、chunk、文档、词表、维度、缓存大小和组件内存等可用字段。
在文件完成索引或服务配置变化后,可以用 **Refresh** 获取新快照。
### 8. 索引管理
Index 页面为派生搜索状态提供明确的维护入口。
![索引管理](https://raw.githubusercontent.com/agentscope-ai/ReMe/main/reme_studio/figures/settings-index.png)
**Rebuild index** 会基于 ReMe 已经摄取的 chunk 重建 BM25、embedding 和 tag 索引。Studio 会在执行前要求确认。该操作不会扫描工作区、重新分块、修改源记忆,也不会重建 wikilink 图谱。这些边界保证 ReMe 的文件仍是唯一事实来源。
### 9. 生效配置
Configuration 显示当前后端返回的完整解析后应用配置。
![生效配置](https://raw.githubusercontent.com/agentscope-ai/ReMe/main/reme_studio/figures/settings-configuration.png)
在文件配置与命令行覆盖完成合并后,可以在这里确认实际工作区、目录布局、服务选项、jobs 和组件后端。ReMe 会在配置到达浏览器之前对敏感字段进行脱敏。
### 10. 版本与服务地址
Version 会显示 ReMe 后端版本和服务地址,并与左上角品牌区域显示的 Studio 版本明确区分。
![版本与服务地址](https://raw.githubusercontent.com/agentscope-ai/ReMe/main/reme_studio/figures/settings-version.png)
这有助于定位前后端版本不一致的问题:顶部栏标识当前安装的 Studio 构建,Settings 标识当前连接的 ReMe 服务。
### 11. 语言与外观
顶部栏可以把整个界面切换为英文或中文。外观支持 **Light**、**Dark** 和 **System**;System 会跟随操作系统的颜色偏好。
![ReMe Studio 深色外观](https://raw.githubusercontent.com/agentscope-ai/ReMe/main/reme_studio/figures/appearance-dark.png)
语言和外观选择会保存在浏览器中。两套主题都复用 ReMe 文档站的品牌色,并为健康状态、选择状态、焦点和图谱保留足够对比度。
## 前端配置
如需持久保存本地覆盖,可以将 `.env.example` 复制为 `.env.local`。vinext/Sites 构建读取 `NEXT_PUBLIC_*`,FastAPI/static 构建读取对应的 `VITE_*` 变量。
| 配置项 | 构建类型 | 作用 |
| --------------------------------------- | ------------ | ------------------------------------------------ |
| `NEXT_PUBLIC_REME_API_URL` | vinext/Sites | ReMe HTTP 服务地址,默认 `http://127.0.0.1:2333` |
| `NEXT_PUBLIC_REME_WORKSPACE_EXTENSIONS` | vinext/Sites | 工作区中显示的扩展名,使用逗号分隔 |
| `VITE_REME_API_URL` | static | ReMe HTTP 服务地址;FastAPI 同源托管时使用 `/` |
| `VITE_REME_WORKSPACE_EXTENSIONS` | static | static 构建使用的工作区扩展名列表 |
默认显示 Markdown 和文本文件。如需增加其他文本格式:
```bash
NEXT_PUBLIC_REME_WORKSPACE_EXTENSIONS=md,txt,mdx
VITE_REME_WORKSPACE_EXTENSIONS=md,txt,mdx
```
只应启用浏览器能够安全视为文本的格式。在 API 边界,ReMe 仍会执行工作区范围限制、允许路径、编码检查、大小限制和乐观修改时间检查。
## 从源码开发
先在仓库根目录启动 ReMe,然后在另一个终端运行前端:
@ -59,15 +257,15 @@ npm install
npm run dev
```
打开 <http://localhost:3000>。前端默认连接 `http://127.0.0.1:2333`,需要时可覆盖该地址:
打开 <http://localhost:3000>。开发前端默认连接 `http://127.0.0.1:2333`,需要时可覆盖:
```bash
NEXT_PUBLIC_REME_API_URL=http://127.0.0.1:8000 npm run dev
```
## 由 ReMe 托管的静态构建
## 构建由 ReMe 托管的静态前端
ReMe 可以通过提供 HTTP API 的同一个 FastAPI 进程托管 Studio。构建静态版本并重启 ReMe:
ReMe 可以使用提供 HTTP API 的同一个 FastAPI 进程托管 Studio。构建静态版本并重启 ReMe:
```bash
cd reme_studio
@ -77,34 +275,47 @@ cd ..
reme start
```
打开 <http://127.0.0.1:2333>。静态构建默认使用同源请求。进行独立的静态开发时,运行
`npm run dev:static`;如有需要,将 `VITE_REME_API_URL` 设置为正在运行的 ReMe 服务地址。
常规的 `npm run build` 命令仍用于 vinext/Sites 部署构建;`npm run build:static` 仅为 FastAPI 和 Python 包分发生成
`dist-static/`。
## 配置
如需持久保存本地覆盖,可以将 `.env.example` 复制为 `.env.local`。vinext/Sites 构建读取
`NEXT_PUBLIC_*`,FastAPI/static 构建读取对应的 `VITE_*` 变量:
| 配置项 | 构建类型 | 作用 |
| --------------------------------------- | ------------ | ------------------------------------------------- |
| `NEXT_PUBLIC_REME_API_URL` | vinext/Sites | ReMe HTTP 服务地址,默认 `http://127.0.0.1:2333` |
| `NEXT_PUBLIC_REME_WORKSPACE_EXTENSIONS` | vinext/Sites | 工作区允许显示的扩展名,使用逗号分隔 |
| `VITE_REME_API_URL` | static | ReMe HTTP 服务地址;由 FastAPI 同源托管时使用 `/` |
| `VITE_REME_WORKSPACE_EXTENSIONS` | static | static 构建使用的工作区扩展名列表 |
工作区会隐藏点文件和点目录,并且默认只显示 Markdown 和文本文件。例如:
打开 <http://127.0.0.1:2333>。静态构建默认使用同源请求。进行独立静态开发时:
```bash
NEXT_PUBLIC_REME_WORKSPACE_EXTENSIONS=md,txt,mdx
VITE_REME_WORKSPACE_EXTENSIONS=md,txt,mdx
VITE_REME_API_URL=http://127.0.0.1:2333 npm run dev:static
```
记忆图谱依赖 ReMe 的摄取流程。在 Studio 设置中重建索引时,只会基于已摄取的 chunks 重建 BM25 和 Embedding 索引;不会扫描工作区文件、重新分块、修改记忆源文件或重建 wikilink 图谱。
`npm run build` 仍用于 vinext/Sites 部署构建;`npm run build:static` 仅为 FastAPI 以及 Python/npm 包分发生成 `dist-static/`。应修改前端源文件,而不是提交生成的分发文件。
## 检查
## 常见问题
### Studio 提示服务不可用
- 确认 `reme start` 仍在运行。
- 检查导航器底部显示的服务地址。
- 前后端分开托管时,请在启动或构建前设置正确的 `NEXT_PUBLIC_REME_API_URL` 或 `VITE_REME_API_URL`。
- 修正地址后,打开 Settings → Status 并点击 Refresh。
### 导航器中缺少文件
- 确认 ReMe 使用预期的 `workspace_dir` 启动。
- 点文件和点目录会被刻意隐藏。
- 如果不是 Markdown 或纯文本,请检查 `*_REME_WORKSPACE_EXTENSIONS`。
- 使用 Files,而不是仅显示部分目录的 Daily 或 Knowledge。
### 图谱为空或不完整
- 确认笔记位于 `digest/wiki`、`digest/personal` 或 `digest/procedure`。
- 确认源文件包含 wikilink,并已被 ReMe 摄取。
- Settings → Rebuild index 只从现有 chunk 重建搜索索引,不会扫描文件或重建图谱。
### Markdown 保存被拒绝
文件在 Studio 加载后被其他进程修改。请保留磁盘上的新版本,重新打开或刷新文件,再应用目标修改并保存。这是防止静默数据丢失的预期冲突保护。
### Chat 无法启动
文件功能可在 Chat 不可用时正常工作。请检查 ReMe 后端配置的 Agent wrapper、模型、API key 和提供商地址,再查看 Settings → Status。不要把凭证放进前端环境变量。
## 验证
在 `reme_studio/` 中运行与改动范围相称的检查:
```bash
npm run format:check
@ -113,3 +324,5 @@ npm run build
npm run build:static
npm test
```
以上截图也构成受支持界面的可视化导览,但前端行为仍以自动化检查为准。

View file

@ -17,7 +17,7 @@
min-width: 0;
flex: 1;
overflow: hidden;
color: #99918a;
color: var(--muted);
font: 11px var(--mono);
text-overflow: ellipsis;
white-space: nowrap;
@ -58,7 +58,7 @@
cursor: pointer;
}
.modeSwitch .modeActive {
color: #8e3b12;
color: var(--accent-strong);
background: var(--soft);
}
.iconBtn {
@ -74,7 +74,7 @@
}
.iconBtn:hover {
color: var(--accent);
border-color: #f2c7ad;
border-color: color-mix(in srgb, var(--accent) 40%, var(--line));
}
.iconBtn:disabled {
opacity: 0.42;

View file

@ -209,7 +209,7 @@
stroke-width: 3px;
}
.labels .activeLabel {
fill: #fff;
fill: var(--accent-foreground);
stroke: var(--accent);
stroke-width: 7px;
font-size: 11px;
@ -298,7 +298,7 @@
cursor: pointer;
}
.openFile:hover {
color: #fff;
color: var(--accent-foreground);
border-color: var(--accent);
background: var(--accent);
}
@ -370,7 +370,7 @@
max-height: 45%;
border: 1px solid var(--line);
border-radius: 10px;
box-shadow: 0 12px 32px rgba(35, 25, 18, 0.18);
box-shadow: 0 12px 32px var(--shadow);
}
.legend {
display: none;

View file

@ -3,35 +3,54 @@
:root {
color-scheme: light;
--bg: #fffdfb;
--panel: #faf7f3;
--surface: #fff;
--subtle: #fbfaf8;
--topbar: #f9f8f4;
--hover: #f0ebe6;
--code-bg: #f3eee9;
--line: #e8e0d8;
--ink: #292522;
--muted: #7b746d;
--accent: #f36b21;
--soft: #fff0e7;
--bg: #ffffff;
--panel: #f4f7f5;
--surface: #ffffff;
--subtle: #f1f6f3;
--topbar: #ffffff;
--hover: #f1f6f3;
--code-bg: #f1f6f3;
--line: #dce5e0;
--ink: #17221d;
--muted: #526159;
--accent: #087f6a;
--accent-foreground: #ffffff;
--accent-strong: #086554;
--accent-bright: #19a98f;
--brand-blue: #3156d9;
--soft: rgba(8, 127, 106, 0.14);
--danger: #d8433e;
--danger-foreground: #000000;
--success: #087f6a;
--shadow: rgba(19, 70, 91, 0.14);
--shadow-strong: rgba(19, 70, 91, 0.24);
--overlay: rgba(9, 32, 25, 0.28);
--mono: var(--font-geist-mono), monospace;
}
:root[data-theme="dark"] {
color-scheme: dark;
--bg: #171513;
--panel: #201d1a;
--surface: #24211e;
--subtle: #211e1b;
--topbar: #1c1917;
--hover: #302b27;
--code-bg: #302b27;
--line: #3b3530;
--ink: #eee9e4;
--muted: #aaa19a;
--soft: #432719;
--bg: #0d1512;
--panel: #09100d;
--surface: #14201b;
--subtle: #17251f;
--topbar: #0d1512;
--hover: #17251f;
--code-bg: #17251f;
--line: #283a33;
--ink: #edf7f3;
--muted: #bacbc4;
--accent: #57dfc3;
--accent-foreground: #0d1512;
--accent-strong: #35c6a9;
--accent-bright: #57dfc3;
--brand-blue: #6575e8;
--soft: rgba(87, 223, 195, 0.14);
--danger: #f07872;
--danger-foreground: #0d1512;
--success: #57dfc3;
--shadow: rgba(2, 14, 10, 0.34);
--shadow-strong: rgba(2, 14, 10, 0.52);
--overlay: rgba(2, 14, 10, 0.58);
}
* {
box-sizing: border-box;
@ -50,7 +69,12 @@ textarea {
}
body {
color: var(--ink);
background: var(--bg);
background: radial-gradient(
circle at 8% 8%,
color-mix(in srgb, var(--accent-bright) 5.5%, transparent),
transparent 28rem
),
var(--bg);
font-size: 14px;
}
button {
@ -66,7 +90,7 @@ textarea:focus-visible {
height: 8px;
}
::-webkit-scrollbar-thumb {
background: #d9d1ca;
background: var(--line);
border: 2px solid transparent;
border-radius: 8px;
background-clip: padding-box;
@ -80,7 +104,7 @@ textarea:focus-visible {
height: 56px;
flex: none;
display: grid;
grid-template-columns: 34px 180px 1fr auto;
grid-template-columns: 34px 220px minmax(0, 1fr) auto;
align-items: center;
gap: 10px;
padding: 0 18px;
@ -91,7 +115,19 @@ textarea:focus-visible {
display: flex;
align-items: center;
font-size: 18px;
font-weight: 600;
font-weight: 750;
white-space: nowrap;
}
.brand-icon {
width: 28px;
height: 28px;
display: block;
flex: none;
background: url("/reme-icon.svg") center / contain no-repeat;
}
.brand-title {
margin-left: 8px;
letter-spacing: -0.02em;
}
.app-version-divider {
width: 1px;
@ -258,7 +294,7 @@ textarea:focus-visible {
border: 1px solid var(--line);
border-radius: 11px;
background: var(--surface);
box-shadow: 0 10px 30px rgba(35, 25, 18, 0.16);
box-shadow: 0 10px 30px var(--shadow);
}
.theme-menu button {
width: 100%;
@ -382,9 +418,8 @@ textarea:focus-visible {
.agent-logo {
display: grid;
place-items: center;
color: var(--accent);
background: var(--soft);
border-radius: 10px;
background: url("/reme-icon.svg") center / contain no-repeat;
}
.source-tabs {
height: 36px;
@ -422,29 +457,29 @@ textarea:focus-visible {
background: var(--hover);
}
.source-tabs button.active {
color: #756ae0;
background: color-mix(in srgb, #756ae0 15%, transparent);
color: var(--brand-blue);
background: color-mix(in srgb, var(--brand-blue) 15%, transparent);
}
.source-tabs .chat-tab {
height: 27px;
margin-left: 2px;
border: 1px solid #f2c7ad;
color: #9a471d;
background: #fff7f1;
border: 1px solid color-mix(in srgb, var(--accent) 32%, var(--line));
color: var(--accent-strong);
background: var(--soft);
}
.source-tabs .chat-tab:hover {
border-color: #e9aa85;
color: #843912;
border-color: var(--accent);
color: var(--accent-strong);
background: var(--soft);
}
:root[data-theme="dark"] .source-tabs .chat-tab {
border-color: #70452f;
color: #f2a170;
background: #342319;
border-color: color-mix(in srgb, var(--accent) 38%, var(--line));
color: var(--accent);
background: var(--soft);
}
:root[data-theme="dark"] .source-tabs .chat-tab:hover {
border-color: #a9633d;
color: #ffc09a;
border-color: var(--accent);
color: var(--accent);
}
.tree {
min-height: 0;
@ -508,7 +543,7 @@ textarea:focus-visible {
flex: 1;
}
.tree-directory-row.graph-active {
background: color-mix(in srgb, #756ae0 12%, transparent);
background: color-mix(in srgb, var(--brand-blue) 12%, transparent);
}
.tree-graph-button {
height: 23px;
@ -527,8 +562,8 @@ textarea:focus-visible {
}
.tree-graph-button:hover,
.tree-directory-row.graph-active .tree-graph-button {
color: #756ae0;
background: color-mix(in srgb, #756ae0 13%, transparent);
color: var(--brand-blue);
background: color-mix(in srgb, var(--brand-blue) 13%, transparent);
}
.side-state {
padding: 18px 10px;
@ -566,16 +601,13 @@ textarea:focus-visible {
height: 7px;
flex: none;
border-radius: 50%;
background: #48a868;
box-shadow: 0 0 0 3px #e1f2e6;
background: var(--success);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--success) 14%, transparent);
}
.status-dot.offline {
background: var(--danger);
box-shadow: 0 0 0 3px #fae5e4;
}
:root[data-theme="dark"] .status-dot {
box-shadow: 0 0 0 3px #263d2d;
}
:root[data-theme="dark"] .status-dot.offline {
box-shadow: 0 0 0 3px #492725;
}
@ -641,7 +673,7 @@ textarea:focus-visible {
border: 1px solid var(--line);
border-radius: 9px;
background: var(--surface);
box-shadow: 0 10px 30px rgba(35, 25, 18, 0.18);
box-shadow: 0 10px 30px var(--shadow);
}
.tab-context-menu button {
width: 100%;
@ -700,6 +732,24 @@ textarea:focus-visible {
font-size: 22px;
letter-spacing: -0.03em;
}
.welcome-brand {
display: flex;
align-items: center;
gap: 7px;
}
.welcome-wordmark {
width: 124px;
height: 42px;
display: block;
flex: none;
background: url("/reme_logo.png") center / contain no-repeat;
}
.welcome-brand > span:last-child {
font-size: 40px;
font-weight: 750;
line-height: 1;
letter-spacing: -0.045em;
}
.welcome p,
.chat-empty p {
margin: 8px 0 22px;
@ -713,13 +763,14 @@ textarea:focus-visible {
gap: 8px;
border: 0;
border-radius: 9px;
color: #fff;
color: var(--accent-foreground);
background: var(--accent);
cursor: pointer;
}
.welcome small {
margin-top: 30px;
color: #a29992;
color: var(--muted);
opacity: 0.72;
}
.markdown {
max-width: 780px;
@ -827,15 +878,21 @@ textarea:focus-visible {
display: grid;
place-items: center;
border-radius: 9px;
color: #fff;
color: var(--accent-foreground);
background: var(--accent);
font-size: 11px;
}
.message.assistant .avatar {
background: color-mix(in srgb, var(--accent) 82%, #fff);
background: transparent;
font-size: 14px;
font-weight: 700;
}
.assistant-logo {
width: 30px;
height: 30px;
display: block;
background: url("/reme-icon.svg") center / contain no-repeat;
}
.message.user .avatar {
color: var(--ink);
background: var(--hover);
@ -928,12 +985,12 @@ textarea:focus-visible {
background: var(--subtle);
}
.stream-block.tool {
background: color-mix(in srgb, #756ae0 9%, var(--panel));
border-color: color-mix(in srgb, #756ae0 35%, var(--line));
background: color-mix(in srgb, var(--brand-blue) 9%, var(--panel));
border-color: color-mix(in srgb, var(--brand-blue) 35%, var(--line));
}
.stream-block.data {
background: color-mix(in srgb, #48a868 8%, var(--panel));
border-color: color-mix(in srgb, #48a868 35%, var(--line));
background: color-mix(in srgb, var(--success) 8%, var(--panel));
border-color: color-mix(in srgb, var(--success) 35%, var(--line));
}
.stream-block.approval {
background: color-mix(in srgb, #d49a2c 9%, var(--panel));
@ -976,7 +1033,7 @@ textarea:focus-visible {
border: 1px solid var(--line);
border-radius: 16px;
background: var(--surface);
box-shadow: 0 8px 28px rgba(35, 25, 18, 0.14);
box-shadow: 0 8px 28px var(--shadow);
transition:
border-color 0.15s,
box-shadow 0.15s,
@ -987,7 +1044,7 @@ textarea:focus-visible {
background: var(--soft);
box-shadow:
0 0 0 3px color-mix(in srgb, var(--accent) 18%, transparent),
0 8px 28px rgba(35, 25, 18, 0.14);
0 8px 28px var(--shadow);
}
.composer textarea {
min-height: 78px;
@ -1007,8 +1064,8 @@ textarea:focus-visible {
flex: none;
border: 0;
border-radius: 10px;
color: #fff;
background: color-mix(in srgb, var(--accent) 82%, #fff);
color: var(--accent-foreground);
background: var(--accent);
cursor: pointer;
}
.composer button:disabled {
@ -1054,7 +1111,7 @@ textarea:focus-visible {
}
@media (max-width: 760px) {
.topbar {
grid-template-columns: 34px 60px 1fr auto;
grid-template-columns: 34px max-content minmax(0, 1fr) auto;
padding: 0 10px;
}
.resource-links,
@ -1071,7 +1128,7 @@ textarea:focus-visible {
bottom: 0;
left: 0;
z-index: 10;
box-shadow: 16px 0 42px rgba(75, 49, 30, 0.16);
box-shadow: 16px 0 42px var(--shadow-strong);
}
.navigator-resize-handle {
display: none;
@ -1080,6 +1137,29 @@ textarea:focus-visible {
left: 0;
}
}
@media (max-width: 480px) {
.topbar {
grid-template-columns: 30px minmax(0, 1fr) auto;
gap: 6px;
padding: 0 8px;
}
.topbar > span {
display: none;
}
.topbar-actions {
grid-column: 3;
gap: 4px;
}
.language-switch button {
min-width: 27px;
padding: 0 4px;
}
.theme-menu summary,
.settings-trigger {
width: 28px;
height: 28px;
}
}
.settings-overlay {
position: fixed;
@ -1090,7 +1170,7 @@ textarea:focus-visible {
place-items: center;
opacity: 0;
pointer-events: none;
background: rgba(30, 25, 21, 0.26);
background: var(--overlay);
backdrop-filter: blur(5px);
transition: opacity 180ms ease;
}
@ -1107,7 +1187,7 @@ textarea:focus-visible {
border: 1px solid color-mix(in srgb, var(--line) 82%, transparent);
border-radius: 20px;
background: var(--surface);
box-shadow: 0 28px 80px rgba(35, 25, 18, 0.26);
box-shadow: 0 28px 80px var(--shadow-strong);
transform: translateY(12px) scale(0.985);
transition: transform 240ms cubic-bezier(0.22, 1, 0.36, 1);
}
@ -1181,12 +1261,8 @@ textarea:focus-visible {
background: var(--hover);
}
.settings-sidebar button.active {
color: #fff;
background: linear-gradient(
135deg,
color-mix(in srgb, var(--accent) 82%, #fff),
var(--accent)
);
color: var(--accent-foreground);
background: linear-gradient(135deg, var(--accent-strong), var(--accent));
box-shadow: 0 5px 14px color-mix(in srgb, var(--accent) 24%, transparent);
}
.settings-sidebar button > span {
@ -1277,19 +1353,19 @@ textarea:focus-visible {
place-items: center;
border-radius: 15px;
color: #fff;
box-shadow: 0 8px 18px rgba(35, 25, 18, 0.14);
box-shadow: 0 8px 18px var(--shadow);
}
.settings-page-icon.status {
background: linear-gradient(145deg, #53b97a, #2f9b5b);
background: linear-gradient(145deg, #19a98f, #087f6a);
}
.settings-page-icon.index {
background: linear-gradient(145deg, #8b80e8, #7065d1);
background: linear-gradient(145deg, #6575e8, #3156d9);
}
.settings-page-icon.config {
background: linear-gradient(145deg, #f29a55, #e7762d);
background: linear-gradient(145deg, #e69a42, #c77724);
}
.settings-page-icon.version {
background: linear-gradient(145deg, #6ca7dc, #477fb7);
background: linear-gradient(145deg, #25a8dc, #159fc9);
}
.status-hero {
position: relative;
@ -1301,14 +1377,14 @@ textarea:focus-visible {
align-items: center;
justify-content: space-between;
gap: 24px;
border: 1px solid color-mix(in srgb, #39a767 24%, var(--line));
border: 1px solid color-mix(in srgb, var(--success) 24%, var(--line));
border-radius: 17px;
background: linear-gradient(
135deg,
color-mix(in srgb, #42a96d 9%, var(--surface)),
color-mix(in srgb, var(--success) 9%, var(--surface)),
var(--surface) 62%
);
box-shadow: 0 5px 18px rgba(35, 25, 18, 0.05);
box-shadow: 0 5px 18px color-mix(in srgb, var(--shadow) 42%, transparent);
}
.status-hero::after {
position: absolute;
@ -1316,7 +1392,7 @@ textarea:focus-visible {
bottom: -92px;
width: 210px;
height: 210px;
border: 1px solid color-mix(in srgb, #42a96d 16%, transparent);
border: 1px solid color-mix(in srgb, var(--success) 16%, transparent);
border-radius: 50%;
content: "";
}
@ -1364,10 +1440,10 @@ textarea:focus-visible {
display: inline-flex;
align-items: center;
gap: 6px;
border: 1px solid color-mix(in srgb, #2f9b5b 25%, transparent);
border: 1px solid color-mix(in srgb, var(--success) 25%, transparent);
border-radius: 999px;
color: #25834a;
background: color-mix(in srgb, #42a96d 9%, transparent);
color: var(--success);
background: color-mix(in srgb, var(--success) 9%, transparent);
font-size: 10px;
font-weight: 600;
white-space: nowrap;
@ -1381,8 +1457,8 @@ textarea:focus-visible {
width: 7px;
height: 7px;
border-radius: 50%;
background: #32a25e;
box-shadow: 0 0 0 3px color-mix(in srgb, #32a25e 14%, transparent);
background: var(--success);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--success) 14%, transparent);
}
.health-badge.unhealthy,
.component-health.unhealthy {
@ -1410,7 +1486,7 @@ textarea:focus-visible {
border: 1px solid var(--line);
border-radius: 15px;
background: var(--surface);
box-shadow: 0 5px 18px rgba(35, 25, 18, 0.05);
box-shadow: 0 5px 18px color-mix(in srgb, var(--shadow) 42%, transparent);
}
.status-metrics small,
.version-card small {
@ -1453,7 +1529,7 @@ textarea:focus-visible {
border: 1px solid var(--line);
border-radius: 14px;
background: var(--surface);
box-shadow: 0 5px 18px rgba(35, 25, 18, 0.04);
box-shadow: 0 5px 18px color-mix(in srgb, var(--shadow) 34%, transparent);
}
.component-card::before {
position: absolute;
@ -1463,7 +1539,7 @@ textarea:focus-visible {
content: "";
}
.component-card.healthy::before {
background: #39a767;
background: var(--success);
}
.component-card header {
min-height: 70px;
@ -1528,7 +1604,7 @@ textarea:focus-visible {
border: 1px solid var(--line);
border-radius: 15px;
background: var(--surface);
box-shadow: 0 5px 18px rgba(35, 25, 18, 0.05);
box-shadow: 0 5px 18px color-mix(in srgb, var(--shadow) 42%, transparent);
}
.settings-card h4 {
margin: 8px 0 10px;
@ -1571,7 +1647,7 @@ textarea:focus-visible {
align-items: center;
gap: 8px;
border: 0;
color: #fff;
color: var(--accent-foreground);
background: var(--accent);
}
.primary-action:disabled {
@ -1602,7 +1678,7 @@ textarea:focus-visible {
background: var(--surface);
}
.index-confirm .danger-action {
color: #fff;
color: var(--danger-foreground);
border-color: var(--danger);
background: var(--danger);
}
@ -1613,8 +1689,8 @@ textarea:focus-visible {
align-items: center;
gap: 8px;
border-radius: 9px;
color: #2f8c52;
background: color-mix(in srgb, #48a868 12%, transparent);
color: var(--success);
background: color-mix(in srgb, var(--success) 12%, transparent);
font-size: 12px;
}
.config-json {

View file

@ -28,7 +28,7 @@ export async function generateMetadata(): Promise<Metadata> {
title: "ReMe Studio",
description:
"Browse, edit, and discuss your local-first ReMe memory workspace.",
icons: { icon: "/favicon.svg", shortcut: "/favicon.svg" },
icons: { icon: "/reme-icon.svg", shortcut: "/reme-icon.svg" },
openGraph: {
title: "ReMe Studio",
description: "本地优先的 Agent 记忆工作区",

View file

@ -22,7 +22,7 @@ import {
SunMoon,
X,
} from "lucide-react";
import { getReMeVersion, readWorkspaceFile, streamChat } from "./api";
import { readWorkspaceFile, streamChat } from "./api";
import { chatStreamError, formatStreamPayloads } from "./chat-stream";
import FilesNavigator from "./files-workspace/FilesNavigator";
import MemoryGraphView from "./files-workspace/MemoryGraphView";
@ -42,6 +42,7 @@ import {
} from "./workspace-drag";
import SettingsCenter from "./settings-center";
import { hasUnsavedChanges, unsavedTabsClosedBy } from "./tab-close";
import packageJson from "../package.json";
const TabbedEditor = dynamic(() => import("./files-workspace/TabbedEditor"), {
ssr: false,
@ -403,9 +404,7 @@ function Chat({ tab }: { tab: Extract<WorkspaceTab, { type: "agent" }> }) {
<div className="messages">
{!tab.messages.length && (
<div className="chat-empty">
<div className="agent-logo">
<Sparkles size={24} />
</div>
<div className="agent-logo" aria-hidden="true" />
<h1>{t("chatTitle")}</h1>
<p>{t("chatDescription")}</p>
<div className="suggestions">
@ -425,9 +424,11 @@ function Chat({ tab }: { tab: Extract<WorkspaceTab, { type: "agent" }> }) {
{message.role === "user" ? (
t("you")
) : (
<span aria-label="ReMe" title="ReMe">
R
</span>
<span
className="assistant-logo"
aria-label="ReMe"
title="ReMe"
/>
)}
</div>
<div className="bubble">
@ -507,7 +508,6 @@ function Workspace() {
const [navOpen, setNavOpen] = useState(true);
const [navigatorWidth, setNavigatorWidth] = useState(260);
const [resizingNavigator, setResizingNavigator] = useState(false);
const [version, setVersion] = useState("");
const [settingsOpen, setSettingsOpen] = useState(false);
const { tabs, activeTabId, openAgent, hydrateMarkdown, failMarkdown } =
useWorkspaceStore();
@ -551,15 +551,6 @@ function Workspace() {
useEffect(() => {
hydrateTheme();
}, [hydrateTheme]);
useEffect(() => {
const controller = new AbortController();
void getReMeVersion()
.then((nextVersion) => {
if (!controller.signal.aborted) setVersion(nextVersion);
})
.catch(() => undefined);
return () => controller.abort();
}, []);
useEffect(() => {
const saved = Number(localStorage.getItem("reme-navigator-width"));
if (!Number.isFinite(saved) || saved <= 0) return;
@ -644,13 +635,10 @@ function Workspace() {
</span>
</button>
<strong>
ReMe Studio
{version && (
<>
<span className="app-version-divider" aria-hidden="true" />
<span className="app-version">v{version}</span>
</>
)}
<span className="brand-icon" aria-hidden="true" />
<span className="brand-title">ReMe Studio</span>
<span className="app-version-divider" aria-hidden="true" />
<span className="app-version">v{packageJson.version}</span>
</strong>
<span>
{active?.type === "markdown"
@ -662,7 +650,7 @@ function Workspace() {
<div className="topbar-actions">
<nav className="resource-links" aria-label={t("documentation")}>
<a
href="https://docs.agentscope.io/reme/latest/en/overview"
href="https://reme.agentscope.io"
target="_blank"
rel="noreferrer"
>
@ -722,8 +710,14 @@ function Workspace() {
<div className="content">
{!active && (
<div className="welcome">
<div className="agent-logo">R</div>
<h1>ReMe Studio</h1>
<h1 className="welcome-brand">
<span
className="welcome-wordmark"
role="img"
aria-label="ReMe"
/>
<span>Studio</span>
</h1>
<p>{t("welcomeDescription")}</p>
<button onClick={openAgent}>
<Sparkles size={16} />

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 227 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 265 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 270 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

View file

@ -7,7 +7,7 @@
name="description"
content="Browse, edit, and discuss your local-first ReMe memory workspace."
/>
<link rel="icon" href="/favicon.svg" />
<link rel="icon" href="/reme-icon.svg" />
<title>ReMe Workspace</title>
<script>
(function () {

View file

@ -1,12 +1,12 @@
{
"name": "@agentscope-ai/reme_studio",
"version": "0.1.1",
"version": "0.1.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@agentscope-ai/reme_studio",
"version": "0.1.1",
"version": "0.1.2",
"license": "Apache-2.0",
"devDependencies": {
"@agentscope-ai/icons": "^1.0.67",

View file

@ -1,6 +1,6 @@
{
"name": "@agentscope-ai/reme_studio",
"version": "0.1.1",
"version": "0.1.2",
"description": "ReMe Studio web workspace and static distribution",
"license": "Apache-2.0",
"files": [

View file

@ -1,13 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<title>ReMe</title>
<rect width="32" height="32" rx="9" fill="#fff0e7"/>
<text
x="16"
y="22.5"
fill="#f36b21"
font-family="Arial, Helvetica, sans-serif"
font-size="20"
font-weight="800"
text-anchor="middle"
>R</text>
</svg>

Before

Width:  |  Height:  |  Size: 321 B

View file

@ -0,0 +1,27 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="ReMe">
<defs>
<linearGradient id="brand" x1="14" y1="12" x2="51" y2="51" gradientUnits="userSpaceOnUse">
<stop stop-color="#21d1bb"/>
<stop offset="0.52" stop-color="#159fc9"/>
<stop offset="1" stop-color="#3156d9"/>
</linearGradient>
<linearGradient id="glass" x1="10" y1="7" x2="56" y2="59" gradientUnits="userSpaceOnUse">
<stop stop-color="#ffffff" stop-opacity="0.94"/>
<stop offset="0.48" stop-color="#e4fffa" stop-opacity="0.82"/>
<stop offset="1" stop-color="#dbe7ff" stop-opacity="0.74"/>
</linearGradient>
<filter id="shadow" x="-30%" y="-30%" width="160%" height="170%">
<feDropShadow dx="0" dy="3" stdDeviation="3" flood-color="#287ca6" flood-opacity="0.22"/>
</filter>
</defs>
<g filter="url(#shadow)">
<rect x="3" y="3" width="58" height="58" rx="18" fill="url(#glass)"/>
<rect x="3.75" y="3.75" width="56.5" height="56.5" rx="17.25" fill="none" stroke="url(#brand)" stroke-opacity="0.46" stroke-width="1.5"/>
<path d="M10 21C18 8 43 7 55 17" fill="none" stroke="white" stroke-width="2.2" stroke-linecap="round" opacity="0.9"/>
</g>
<path d="M18 49V15h14.5C41 15 46 19.5 46 27s-5 12-13.5 12H27" fill="none" stroke="url(#brand)" stroke-width="7.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M27 38.5 46 49" fill="none" stroke="url(#brand)" stroke-width="7.5" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="27" cy="38.5" r="2.2" fill="white" stroke="#43d7c5" stroke-width="0.8"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 283 KiB

View file

@ -1,6 +1,6 @@
[project]
name = "reme_studio"
version = "0.1.1"
version = "0.1.2"
description = "Optional ReMe Studio static frontend."
readme = "README.md"
license = "Apache-2.0"

View file

@ -0,0 +1,120 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
test("Studio reuses the canonical ReMe brand assets", async () => {
const pairs = [
["../../docs/figure/reme-icon.svg", "../public/reme-icon.svg"],
["../../docs/figure/reme_logo.png", "../public/reme_logo.png"],
];
for (const [canonical, studio] of pairs) {
assert.deepEqual(
await readFile(new URL(canonical, import.meta.url)),
await readFile(new URL(studio, import.meta.url)),
);
}
});
test("Studio theme uses the documentation site's canonical palette", async () => {
const css = await readFile(
new URL("../app/globals.css", import.meta.url),
"utf8",
);
for (const color of [
"#087f6a",
"#086554",
"#19a98f",
"#3156d9",
"#ffffff",
"#f4f7f5",
"#17221d",
"#57dfc3",
"#0d1512",
"#edf7f3",
]) {
assert.match(css, new RegExp(color));
}
});
test("solid theme actions use foreground colors with sufficient contrast", async () => {
const css = await readFile(
new URL("../app/globals.css", import.meta.url),
"utf8",
);
const graphCss = await readFile(
new URL("../app/files-workspace/memory-graph.module.css", import.meta.url),
"utf8",
);
const tokens = [
...css.matchAll(/:root(?:\[data-theme="dark"\])?\s*\{([^}]+)\}/g),
].map(([, declarations]) =>
Object.fromEntries(
[...declarations.matchAll(/--([\w-]+):\s*(#[\da-f]{6});/gi)].map(
([, name, value]) => [name, value],
),
),
);
const luminance = (color) => {
const channels = [1, 3, 5]
.map(
(offset) => Number.parseInt(color.slice(offset, offset + 2), 16) / 255,
)
.map((channel) =>
channel <= 0.04045
? channel / 12.92
: ((channel + 0.055) / 1.055) ** 2.4,
);
return 0.2126 * channels[0] + 0.7152 * channels[1] + 0.0722 * channels[2];
};
const contrast = (first, second) => {
const values = [luminance(first), luminance(second)].sort(
(left, right) => right - left,
);
return (values[0] + 0.05) / (values[1] + 0.05);
};
assert.equal(tokens.length, 2);
for (const theme of tokens) {
assert.ok(contrast(theme.accent, theme["accent-foreground"]) >= 4.5);
assert.ok(contrast(theme.danger, theme["danger-foreground"]) >= 4.5);
}
for (const selector of [
".welcome > button",
".composer button",
".settings-sidebar button.active",
".primary-action",
]) {
const rule =
css.match(
new RegExp(`${selector.replaceAll(".", "\\.")} \\{[^}]+\\}`),
)?.[0] || "";
assert.match(rule, /color: var\(--accent-foreground\);/, selector);
}
assert.match(graphCss, /fill: var\(--accent-foreground\);/);
assert.match(graphCss, /color: var\(--accent-foreground\);/);
assert.match(css, /color: var\(--danger-foreground\);/);
});
test("Studio package versions stay aligned", async () => {
const packageJson = JSON.parse(
await readFile(new URL("../package.json", import.meta.url), "utf8"),
);
const packageLock = JSON.parse(
await readFile(new URL("../package-lock.json", import.meta.url), "utf8"),
);
const pyproject = await readFile(
new URL("../pyproject.toml", import.meta.url),
"utf8",
);
assert.equal(packageJson.version, "0.1.2");
assert.equal(packageLock.version, packageJson.version);
assert.equal(packageLock.packages[""].version, packageJson.version);
assert.match(
pyproject,
new RegExp(`^version = "${packageJson.version}"$`, "m"),
);
});

View file

@ -1,4 +1,5 @@
import assert from "node:assert/strict";
import { readFile } from "node:fs/promises";
import test from "node:test";
async function render() {
@ -21,7 +22,14 @@ test("server-renders the ReMe Studio shell", async () => {
assert.match(response.headers.get("content-type") ?? "", /^text\/html\b/i);
const html = await response.text();
const packageJson = JSON.parse(
await readFile(new URL("../package.json", import.meta.url), "utf8"),
);
assert.match(html, /<title>ReMe Studio<\/title>/i);
assert.match(
html.replaceAll("<!-- -->", ""),
new RegExp(`v${packageJson.version}`),
);
assert.match(html, /新建 Agent 对话/);
assert.match(html, /文件保留在你的本地工作区/);
assert.doesNotMatch(