Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -637,6 +637,20 @@ SidePanel 折叠 UI:`ChatView` 把 `SidePanel` 包到 `<div class="ace-side-pane

**中断回合后队列暂停,不自动出队。** 曾经的 bug:排了一堆消息后点停止,busy 一翻 false,ChatView 的 drain effect 就把下一条排队消息发了出去 —— 用户刚说「停」界面却替他继续。现在队列状态多一份按会话的 `paused`(`chatInputQueue.js::pauseQueuedInput / resumeQueuedInput / queuedInputPause`),`shouldDrainQueuedInput` 带 `paused` 时拒绝 drain;暂停有两条入口:(1) 本端点停止 —— `ChatView::abort()` **先** `pauseQueuedInput` 再本地应用 `turn_aborted`,顺序反了 drain effect 会先看到 busy=false 把下一条发出去;(2) TUI / 其它标签页 / IM 通道发起的中断只以 `outcome=aborted` 的 `busy_changed` / `done` 到达,靠 transcript reducer 新增的 `lastTurnOutcome`(`'' | completed | error | aborted`,新回合开始清空,只在真正的 busy→false 转换上按 outcome 记;本地 `turn_aborted` 直接记 aborted,服务端补发的 busy_changed(false) 到达时 wasBusy 已 false 不覆盖)+ `shouldPauseQueuedInputAfterAbort` 在 drain effect 里兜底。界面:`QueueCardList` 顶部横幅「由于你中断了当前响应,队列已暂停」+「继续」(`queueCardItem.js::buildQueuePausedBanner`);输入栏空输入 + 暂停时发送按钮变「继续」(`inputBarState.js` 的 `mode='resume'`,压过「重发末尾用户消息」,图标换播放三角,`InputBar::submit` 走 `onResumeQueue` 而不是 `onSubmit`)。解除暂停只认用户明确动作:点「继续」/ 空输入按发送 / 再次发送或排队一条新消息(新消息先走,旧排队随后照常出队)/ 重试失败卡片;删掉最后一张卡片时暂停标记顺带清除,避免一个看不见的暂停态把之后的排队卡住。`pauseQueuedInput` 对没有待发送消息的会话是 no-op。回归:`chatInputQueue.test.js` 的暂停一组、`inputBarState.test.js` 的「继续」两条、`queueCardItem.test.js` 的横幅两条、`sessionTranscript.test.js` 的 `lastTurnOutcome` 两条。

### Web UI: HUD 主题外观与多风格(荒坂公司主题,2026-10-24)

**这是创意工坊主题专用能力,默认不启用:用户不安装并选用 HUD 主题就完全不生效。用户拍板:不写进版本更新记录(`aceupdate.json` 的 `notes`、Release 说明),发版整理更新内容时跳过这部分提交。**

主题包顶层可选 `chrome:{style:"hud"}` 与 `variants:[{id,name,colors(28),appearance?,tint?}]`(2–6 项,第一项为默认),格式见 [docs/themes.md](docs/themes.md#hud-外观与多风格)。**两个字段刻意放顶层、`appearance` 不加键**:C++ `valid_theme_definition`、已部署工坊的 `WorkshopPackage.cs` 都拒绝未知 appearance 键却忽略未知顶层键,放进 appearance 会让旧版 ACECode 和线上工坊整包拒收。前端 `themePackages.js` 对二者「不认识就忽略」(`themeChromeStyle` / `themeVariants` 全有或全无),不影响旧主题。

- **样式全在 `web/src/styles/themeHud.css`**,每条选择器必须以 `:root[data-theme-chrome="hud"]` 开头(或是只在 HUD 下挂载的 `.ace-hud-*` 动效层类名),动画还必须带 `[data-theme-effects="on"]`;`themeHud.test.js` 解析 CSS 守着这两条。它随应用常驻加载,一条没加作用域的规则就会改到所有主题。
- 切角:无弹层的元素(用户气泡、代码块、首页卡片)直接 `clip-path`;可能挂下拉/悬浮卡片的容器(输入框 `.ace-composer-card`、工具行 `.ace-tool-activity`)把裁切底色放 `::before`,容器本身不裁,否则会把弹层切掉。框线统一画在 `::after` 的渐变层上(`--hud-frame` / `--hud-cut` 覆盖)。
- **背景染色**:`tint` 让三张背景按 CSS `background-blend-mode: color` 染色(保留亮度、换色相),一套图对应全部风格;`themeCssProperties(..., {tint})` 插入同色渐变层并给出 `--ace-*-background-blend`,无 tint 时输出与旧版逐字一致。用户消息气泡里图片之外的底色同样被染,图片顶部纯色与 `user_message_background_color` 亮度相同才无接缝。
- 首页 logo 的 `::before` 被暗色主题规则占着(translate、径向光晕、遮罩),HUD 重新启用它时必须把 `transform/background/mask` 显式清掉,否则环偏移两次、留下一团光斑。
- 动效层 `components/HudEffects.jsx`(ThemeProvider 用 props 渲染,别改成 useTheme——会形成 theme.jsx ↔ HudEffects 循环引用):扫描光带 z-index 90 在弹窗下,开屏/切换过场单独一层 z-index 2147483000 盖在设置窗口之上(否则在设置里切风格看不到过场)。触发判断在 `lib/hudEffects.js`,开屏后 1.5 秒内的风格变化视为 daemon 恢复已保存风格,不再叠过场;每个 burst 自带计时器。Agent 运行中靠发送按钮的 `data-composer-action="stop"` 用 `:has()` 加速输入框流光。
- 风格与动效开关存 `config.json` 的 `web_ui.theme_variant` / `web_ui.theme_effects`,走 `/api/config/ui-preferences` 与 Desktop 首帧注入(端口每次启动都变,不能只靠 localStorage)。动效**不**跟随系统减少动态效果(用户常在 RDP 里用,见陪伴宠物同款决定),以开关为准。亮色强调色(黄/青)上白字不可读,HUD 下 `.bg-accent.text-white` 改用 `--ace-bg`。
- 素材生成脚本与打包流程不在仓库里:徽标几何取自 Wikimedia Commons 的 CC0 `Arasaka_logo_lite.png`(浏览器里距离变换量出),首页天际线由 Codex 生成后嵌入发光徽标。回归:`web/src/lib/themeHud.test.js`、`colorThemeArchitecture.test.js`、`appearancePreferences.test.js`、`tests/config/config_web_ui_preferences_test.cpp` 的 ThemeVariant 三条、`web_server_smoke_test.cpp::UiPreferencesThemeVariantAndEffectsRoundTrip`。

### Web UI: 粘贴的文本块(fix-feedback-0924 第 2 条)

起因(LIUXIN557 f300):一条 2460 万字符的粘贴原样进了用户气泡、回合滚动条和吸顶条,切进会话必然卡死。用户不接受输入限长,决定:达到 20 行 / 2000 字符的粘贴变成输入框上方的卡片(点开在单独文本框编辑);很大的粘贴落文件、消息里只带引用。
Expand Down
13 changes: 12 additions & 1 deletion docs/daemon-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -3719,7 +3719,9 @@ Returns:
"color_theme": "blue",
"font_size": "medium",
"sidebar_session_time": true,
"message_auto_collapse": true
"message_auto_collapse": true,
"theme_variant": "",
"theme_effects": true
}
```

Expand All @@ -3734,6 +3736,15 @@ configurations. When `false`, main and subagent conversations display messages
without activity/turn folding; individual tool calls remain collapsible.
The preference is persisted and included in the Desktop appearance bootstrap.

`theme_variant` selects a variant inside the active theme package (see
[themes.md](themes.md#hud-外观与多风格)). It is empty or 1-24 lowercase ASCII
letters/digits joined by single hyphens; empty means the package's first
variant, and an ID the active theme does not declare also falls back to it.
`theme_effects` (default `true`) turns the motion of HUD-chrome themes on or
off and does not follow the OS reduced-motion setting. Both are persisted,
included in the Desktop appearance bootstrap, and rejected with `400` when
malformed.

### `PUT /api/config/ui-preferences`

Body:
Expand Down
30 changes: 30 additions & 0 deletions docs/themes.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,36 @@ AI 主题使用 `ai-<slug>` 标识,允许小写 ASCII 字母、数字及短横

草稿存放在 `themes/drafts`;安装后的文件位于 `themes/<id>/<version>/`,通过原子更新 `installed.json` 发布;新 ZIP 位于 `themes/exports/<id>/<version>.zip`。旧版平铺 ZIP 在验证包内主题 ID、版本和内容后仍可复用,删除也按实际归属核验,避免 ID 与版本拼接相似时误删其它主题。确认绑定配色及图片哈希,修改内容后必须重新确认。工具和列表字段详见 [Daemon API](daemon-api.md#ai-theme-workflow-tool)。

## HUD 外观与多风格

主题包可以在顶层(与 `colors`、`appearance` 同级)声明两个可选字段:

```json
"chrome": {"style": "hud"},
"variants": [
{"id": "red", "name": "荒坂", "colors": {"…": "完整 28 色"}},
{"id": "cyan", "name": "公共区", "colors": {"…": "完整 28 色"}, "tint": "#22CCF2", "appearance": {"logo_color": "#2BD3F5"}},
{"id": "yellow", "name": "限制区", "colors": {"…": "完整 28 色"}, "tint": "#F2E022"}
]
```

- `chrome.style` 目前只有 `hud`。切角面板、霓虹描边、侧栏刻度、会话标尺、带凸台的输入框、代码块标题条和语法配色都由 ACECode 自带的样式(`web/src/styles/themeHud.css`)实现,颜色取自当前配色:线条用 `accent`,选中态与发送按钮用 `send-bg`。主题包仍然只含数据,不含 CSS、HTML 或脚本;不认识的 style 直接忽略,按普通主题显示。
- `variants` 为 2–6 项,全部合法才生效,任意一项有问题就整组忽略,避免悄悄改变默认项。第一项是默认风格,应与顶层 `colors` 一致;每项都带完整 28 色。`appearance` 按键覆盖顶层外观,`tint` 让三张背景图按 CSS `color` 混合染色:保留图片明暗,换成 tint 的色相,所以一套图片就能对应全部风格。不带 tint 的风格按原图显示。
- 染色作用于整块背景区域,包括用户消息气泡里图片之外的底色。只要图片顶部纯色与 `user_message_background_color` 亮度一致,长消息就不会出现接缝。
- 兼容性:两个字段都在顶层,旧版 ACECode 和已部署的工坊校验都会忽略它们,旧版显示为默认风格的普通主题;`appearance` 没有新增键。
- 设置 → 外观:当前主题带风格时显示「主题风格」,声明 HUD 时显示「动态效果」开关。选择保存在 `config.json` 的 `web_ui.theme_variant` / `web_ui.theme_effects`(见 [Daemon API](daemon-api.md#get-apiconfigui-preferences)),Desktop 首帧注入,换端口不会丢。
- 动效只在 HUD 主题且「动态效果」开启时运行:
- 主题生效时的一次开屏扫描;
- 切换风格时的故障横条和三角警告过场,显示在设置窗口之上;
- 缓慢下移的扫描光带;
- 会话水印与首页壁纸的霓虹闪烁;
- 首页图标外的瞄准环转动;
- 选中任务的呼吸光;
- 悬停时的抖动;
- 输入框聚焦时沿底边的流光,Agent 运行中流光加速。

动画只用 transform、opacity 或小面积重绘,不跟随系统的「减少动态效果」设置(远程桌面关闭窗口动画时会误报),以设置里的开关为准。

## Built-in National Day theme

The built-in `national-day-2026` theme is the approved ACECode-created
Expand Down
2 changes: 2 additions & 0 deletions src/apps/desktop/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2272,6 +2272,8 @@ int main(int argc, char** argv) {
{"font_size", desktop_cfg.web_ui.font_size},
{"sidebar_session_time", desktop_cfg.web_ui.sidebar_session_time},
{"message_auto_collapse", desktop_cfg.web_ui.message_auto_collapse},
{"theme_variant", desktop_cfg.web_ui.theme_variant},
{"theme_effects", desktop_cfg.web_ui.theme_effects},
}.dump();
const std::string startup_bootstrap = startup_timeline.snapshot_json();
host.init_script(acecode::desktop::locale_bootstrap_script(
Expand Down
24 changes: 24 additions & 0 deletions src/apps/web/routes/routes_misc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2194,6 +2194,22 @@ void WebServer::Impl::register_ui_preferences() {
"sidebar_session_time must be a boolean");
}
}
if (body.contains("theme_variant")) {
has_supported_field = true;
if (!body["theme_variant"].is_string() ||
!is_valid_web_ui_theme_variant(
body["theme_variant"].get<std::string>())) {
return json_err(400, "BAD_REQUEST",
"theme_variant must be empty or a lowercase variant ID");
}
}
if (body.contains("theme_effects")) {
has_supported_field = true;
if (!body["theme_effects"].is_boolean()) {
return json_err(400, "BAD_REQUEST",
"theme_effects must be a boolean");
}
}
if (!has_supported_field) {
return json_err(400, "BAD_REQUEST",
"no supported UI preference field was provided");
Expand Down Expand Up @@ -2234,6 +2250,14 @@ void WebServer::Impl::register_ui_preferences() {
deps.app_config->web_ui.sidebar_session_time =
body["sidebar_session_time"].get<bool>();
}
if (body.contains("theme_variant")) {
deps.app_config->web_ui.theme_variant =
body["theme_variant"].get<std::string>();
}
if (body.contains("theme_effects")) {
deps.app_config->web_ui.theme_effects =
body["theme_effects"].get<bool>();
}
try {
if (!deps.config_path.empty()) {
save_config(*deps.app_config, deps.config_path);
Expand Down
2 changes: 2 additions & 0 deletions src/apps/web/server_helpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,8 @@ json ui_preferences_to_json(const WebUiPreferencesConfig& cfg) {
{"font_size", cfg.font_size},
{"sidebar_session_time", cfg.sidebar_session_time},
{"message_auto_collapse", cfg.message_auto_collapse},
{"theme_variant", cfg.theme_variant},
{"theme_effects", cfg.theme_effects},
};
}

Expand Down
37 changes: 37 additions & 0 deletions src/base/config/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,18 @@ bool is_valid_web_ui_font_size(const std::string& font_size) {
font_size == "large";
}

bool is_valid_web_ui_theme_variant(const std::string& variant) {
if (variant.empty()) return true;
if (variant.size() > 24 || variant.front() == '-' || variant.back() == '-') return false;
char previous = 0;
for (const char c : variant) {
const bool word = (c >= 'a' && c <= 'z') || (c >= '0' && c <= '9');
if (!word && (c != '-' || previous == '-')) return false;
previous = c;
}
return true;
}

nlohmann::json connectors_to_json(const std::vector<ConnectorConfig>& connectors) {
nlohmann::json items = nlohmann::json::array();
for (const auto& connector : connectors) {
Expand Down Expand Up @@ -500,6 +512,9 @@ std::vector<std::string> validate_config(const AppConfig& cfg) {
if (!is_valid_web_ui_font_size(cfg.web_ui.font_size)) {
errors.push_back("web_ui.font_size must be one of: small, medium, large");
}
if (!is_valid_web_ui_theme_variant(cfg.web_ui.theme_variant)) {
errors.push_back("web_ui.theme_variant must be empty or a lowercase variant ID");
}
if (cfg.web.remote_port < 0 || cfg.web.remote_port > 65535) {
errors.push_back(
"web.remote_port out of range (0-65535): " +
Expand Down Expand Up @@ -1084,6 +1099,24 @@ static AppConfig load_config_from_path_once(
LOG_WARN("[config] invalid 'web_ui.sidebar_session_time', using true");
}
}
if (uij.contains("theme_variant")) {
if (uij["theme_variant"].is_string() &&
is_valid_web_ui_theme_variant(
uij["theme_variant"].get<std::string>())) {
cfg.web_ui.theme_variant =
uij["theme_variant"].get<std::string>();
} else {
LOG_WARN("[config] invalid 'web_ui.theme_variant', using the theme default");
}
}
if (uij.contains("theme_effects")) {
if (uij["theme_effects"].is_boolean()) {
cfg.web_ui.theme_effects =
uij["theme_effects"].get<bool>();
} else {
LOG_WARN("[config] invalid 'web_ui.theme_effects', using true");
}
}
}
}
if (j.contains("models_dev") && j["models_dev"].is_object()) {
Expand Down Expand Up @@ -2330,6 +2363,10 @@ nlohmann::json build_config_json(const AppConfig& cfg) {
web_uij["sidebar_session_time"] = cfg.web_ui.sidebar_session_time;
if (cfg.web_ui.message_auto_collapse != web_ui_d.message_auto_collapse)
web_uij["message_auto_collapse"] = cfg.web_ui.message_auto_collapse;
if (cfg.web_ui.theme_variant != web_ui_d.theme_variant)
web_uij["theme_variant"] = cfg.web_ui.theme_variant;
if (cfg.web_ui.theme_effects != web_ui_d.theme_effects)
web_uij["theme_effects"] = cfg.web_ui.theme_effects;
if (!web_uij.empty()) j["web_ui"] = std::move(web_uij);

if (auto memj = memory_config_to_json(cfg.memory); !memj.empty()) j["memory"] = std::move(memj);
Expand Down
7 changes: 7 additions & 0 deletions src/base/config/config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,11 @@ struct WebUiPreferencesConfig {
bool sidebar_session_time = true;
// Collapse conversation activity; false keeps only individual tools foldable.
bool message_auto_collapse = true;
// Variant ID inside the selected theme package (empty = its first variant).
// The renderer falls back to the default when the active theme lacks it.
std::string theme_variant;
// Motion for themes that declare an animated chrome style; default on.
bool theme_effects = true;
};

struct ModelsDevConfig {
Expand Down Expand Up @@ -687,6 +692,8 @@ bool is_valid_ui_locale(const std::string& locale);
bool is_valid_web_ui_theme(const std::string& theme);
bool is_valid_web_ui_color_theme(const std::string& color_theme);
bool is_valid_web_ui_font_size(const std::string& font_size);
// Empty, or 1-24 chars of lowercase ASCII words joined by single hyphens.
bool is_valid_web_ui_theme_variant(const std::string& variant);

nlohmann::json connectors_to_json(const std::vector<ConnectorConfig>& connectors);
bool parse_connectors_json(const nlohmann::json& value,
Expand Down
Loading
Loading