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: 7 additions & 7 deletions .agents/skills/acecode-frontend-style/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,12 @@ ACECode 只使用两套字体栈,统一由 `web/src/styles/globals.css` 的 Tail

## 3. 一级分组(同 section 里的分组标题 + 描述)

主标题 + 灰色描述,主标题 → 描述 → 内容 之间是 `mb-1` / `mb-3`:
**Avoid unnecessary secondary labels and descriptions. Let the layout do the explaining.**

默认只显示分组标题,以间距、对齐和控件本身解释结构。只有影响决策、非显而易见的行为或错误恢复才补充一句简短说明,不复述标题、开关或预览,不改为默认 tooltip。必要说明使用 `text-[12px] text-fg-mute mb-3`。

```jsx
<div className="text-[14px] font-semibold mb-1">权限模式</div>
<p className="text-[12px] text-fg-mute mb-3">控制 Agent 调用工具时的确认行为</p>
<div className="text-[14px] font-semibold mb-3">权限模式</div>
{/* 行卡片 / 卡片网格 */}
```

Expand All @@ -69,13 +70,12 @@ ACECode 只使用两套字体栈,统一由 `web/src/styles/globals.css` 的 Tail
<div className="flex items-center justify-between px-3.5 py-2.5 rounded-md bg-surface border border-border mb-2">
<div>
<div className="text-[13px] font-normal text-fg">最大轮次</div>
<div className="text-[11px] text-fg-mute mt-0.5">单次 agent loop 的最大迭代数</div>
</div>
{/* 右侧:Toggle / 小 input / status pill / 下拉 / 按钮 */}
</div>
```

- 左侧主标 13px normal + fg、副描述 11px fg-mute、行间 `mt-0.5`
- 左侧主标 13px normal + fg;必要的副描述才使用 11px fg-mute 和 `mt-0.5`,不默认添加。
- 卡片间隙 `mb-2`,padding `px-3.5 py-2.5`
- 圆角 `rounded-md`,描边 `border-border`,底色 `bg-surface`

Expand Down Expand Up @@ -262,9 +262,9 @@ section 内不同子分组之间:
写新 settings section / 类似管理面板时:

- [ ] H2 用 `text-xl font-bold mb-5`
- [ ] 一级分组标题 `text-[14px] font-semibold mb-1` + 描述 `text-[12px] text-fg-mute mb-3`
- [ ] 一级分组只保留明确标题与合适间距;必要描述使用 `text-[12px] text-fg-mute mb-3`
- [ ] 仅主标题、一级标题可加粗;字段、卡片、选项、导航及更深层标题均为 400,强调使用文字颜色
- [ ] 配置项用 § 4 行卡片(左 13px normal + fg / 11px fg-mute,右 toggle/input/pill)
- [ ] 配置项用 § 4 行卡片(左 13px normal + fg,右 toggle/input/pill),不默认添加次级描述
- [ ] 多选一用 § 4.1 row-radio,多选卡用 § 5 grid
- [ ] input 走 § 6.0 / § 6.1 模板
- [ ] 状态指示走 § 8 绿点
Expand Down
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Repository Guidelines

## UI 文案强约束

**Avoid unnecessary secondary labels and descriptions. Let the layout do the explaining.**

所有界面默认只保留完成操作所需的标题、标签、内容与操作。不要给每个标题、字段、开关或预览附加解释,也不要用说明文字复述布局或控件已经表达的信息。优先通过分组、间距、对齐和明确命名表达关系;只有影响用户决策、非显而易见的行为、必要风险或错误恢复才保留简短说明。不得把删除的冗余文案转移为默认 tooltip。无障碍名称和必要的错误信息必须保留。此约束优先于样式示例中的辅助文案。

## src 分层搬迁交付记录

2026-09-29 用户最新决定:取消等待 06:00 候选窗口,一期现已完成约定的 Windows 验收,立即提交并 push。原公告中的最早开始时间由本条覆盖;通过受保护 master 所需的 PR 完成交付,九个旧 ref/worktree 与已批准的补验安排保持。
Expand Down
6 changes: 6 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# CLAUDE.md

## UI 文案强约束

**Avoid unnecessary secondary labels and descriptions. Let the layout do the explaining.**

遵守 [AGENTS.md 的 UI 文案强约束](AGENTS.md#ui-文案强约束):通过分组、间距、对齐和明确命名表达界面关系;不默认添加次级标签、描述或 tooltip,保留必要的决策信息、错误恢复和无障碍名称。

## src 分层搬迁交付记录

2026-09-29 用户最新决定:取消等待 06:00 候选窗口,一期现已完成约定的 Windows 验收,立即提交并 push。原公告中的最早开始时间由本条覆盖;通过受保护 master 所需的 PR 完成交付,九个旧 ref/worktree 与已批准的补验安排保持。
Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
set(VCPKG_TARGET_TRIPLET "x64-windows-static" CACHE STRING "Vcpkg target triplet" FORCE)
endif()

project(acecode VERSION 1.0.14 LANGUAGES C CXX)
project(acecode VERSION 1.0.15 LANGUAGES C CXX)

include(${CMAKE_SOURCE_DIR}/cmake/acecode_source_guards.cmake)

Expand Down Expand Up @@ -105,7 +105,7 @@
set(ACECODE_WEB_DIST "${CMAKE_SOURCE_DIR}/web/dist")
set(ACECODE_WEB_EMBED_DIR "${ACECODE_WEB_DIST}")
if(NOT EXISTS "${ACECODE_WEB_DIST}")
message(WARNING

Check warning on line 108 in CMakeLists.txt

View workflow job for this annotation

GitHub Actions / unit-tests (linux-x64)

[acecode] web/dist/ not found — embedding a minimal fallback page. Run
"[acecode] web/dist/ not found — embedding a minimal fallback page. "
"Run `pnpm install && pnpm build` inside web/ before re-configuring for the full UI.")
set(ACECODE_WEB_EMBED_DIR "${CMAKE_BINARY_DIR}/generated/web-dist-fallback")
Expand Down
40 changes: 38 additions & 2 deletions docs/daemon-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -4623,7 +4623,7 @@ Persists the default and updates the in-memory session registry default.
Returns:

```json
{"base_url":"https://example.com/acecode"}
{"base_url":"https://example.com/acecode/","auto_update":false}
```

### `PUT /api/config/upgrade`
Expand All @@ -4634,7 +4634,9 @@ Body:
{"base_url":"https://example.com/acecode"}
```

Normalizes and validates a non-empty HTTP(S) base URL.
Accepts a partial object containing `base_url` and/or boolean `auto_update`.
Normalizes and validates a non-empty HTTP(S) base URL and preserves omitted
fields. Automatic installation defaults to false, including for old configs.

### `GET /api/update/status`

Expand Down Expand Up @@ -4720,6 +4722,22 @@ with U+FFFD instead of failing the request, so a failed job with such text in

### `POST /api/update/start`

An optional JSON body `{"automatic":true}` marks an automatic installation.
It is rejected with `409 AUTO_UPDATE_DISABLED` unless the persisted setting
is enabled, checked again after the manifest request. Empty bodies keep manual
behavior. Jobs return `automatic` and `automatic_failures` (0–3). The latter
counts consecutive automatic installation failures for the same target in the
current daemon lifetime; successful installation or a new target resets it.
Precheck and progress-query failures are not installation failures.

The GUI keeps the startup check and then checks every independently randomized
10–120 minutes regardless of this setting. Automatic check failures are silent
and preserve the last valid status. Automatic installation does not open a
dialog or restart the app. Success shows `更新已完成,重启后生效`; failure is
silent until three consecutive failures change the sidebar label to `更新失败`.
Manual retry remains available. Progress transport errors retry without
synthesizing a failed installation.

Checks for an update and starts one daemon-managed background update job. The
job reuses the normal upgrade engine without creating a console window. Returns
`202`:
Expand Down Expand Up @@ -6262,3 +6280,21 @@ Backs the `ios_simulator` / `ios_simulator_build` tools, Settings > Tools > iOS
Tool results of `ios_simulator` carry `metadata.ios_simulator = {action, udid, name, runtime, point_width, point_height, rotation}`; the web UI opens the panel on `attach` and closes it on `detach`.

Runtime-generated names use underscore separators. Existing hyphen-named data remains accessible; see [运行数据命名](runtime_file_naming.md).

## 主页意图提示

`GET /api/home/intent-prompts` 返回当前进程缓存;`POST /api/home/intent-prompts`
接收 `{ "force": false }`,到期时生成建议。`force: true` 用于 Ctrl+Y 调试刷新。
两者均遵循标准认证。响应包含 `suggestions`、`model`、`refreshing`、`error`、
`generated_at_ms` 和 `next_refresh_at_ms`。并发 POST 合并,`refreshing: true`
时客户端可短暂读取状态直至完成,不重复 POST。常规刷新周期为 30 分钟。

建议包含 `category`(writing / exploration / execution / thinking)、`title`、
`description`、`prompt`、`source_session_id`、`workspace: {hash,cwd,name,noWorkspace}`。
来源是最近真实用户活动的最多 5 个非运行、非归档主会话;最多返回 4 条建议。
后端绑定工作区,不采信模型输出的路径。点击只预填草稿及选择工作区。

模型优先使用 `summary_generation.model_name`,未选择时采用最近成功生效的
会话模型(元数据 `model_activated_at_ms`;旧记录按最后用户消息兼容)。
摘要模型已选择但无效不静默回退。`MODEL_UNAVAILABLE`、`TIMEOUT`、
`GENERATION_FAILED`、`INVALID_RESPONSE` 均保留旧结果。推断不创建会话、不调用工具。
174 changes: 8 additions & 166 deletions src/adapters/themes/theme_store.cpp
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
#include "theme_store.hpp"
#include "theme_validation.hpp"
#include "utils/base64.hpp"
#include "theme_package.hpp"
#include "utils/semver.hpp"
#include "image/image_processor.hpp"
#include "utils/atomic_file.hpp"
#include "utils/sha256.hpp"
#include "utils/utf8_path.hpp"
#include "utils/generated_file_names.hpp"
#include "utils/uuid.hpp"

#include <algorithm>
#include <array>
#include <chrono>
#include <cctype>
#include <fstream>
Expand All @@ -20,11 +19,17 @@
#include <set>
#include <vector>
#include <zip.h>
#include <curl/curl.h>

namespace acecode::themes {
namespace fs = std::filesystem;
using nlohmann::json;
using detail::hex;
using detail::version_ok;
using detail::asset_ok;
using detail::png;
using detail::validate_theme_files;
using detail::kMaxPackageBytes;
using detail::kMaxPreviewBytes;

struct ThemeRootState {
std::recursive_mutex mutex;
Expand All @@ -44,8 +49,6 @@ struct ThemeExportJob {
};

namespace {
constexpr std::uintmax_t kMaxPackageBytes = 16 * 1024 * 1024;
constexpr std::uintmax_t kMaxPreviewBytes = 256 * 1024;
constexpr std::size_t kMaxExportJobs = 32;

std::shared_ptr<ThemeRootState> shared_root_state(const fs::path& root) {
Expand Down Expand Up @@ -139,38 +142,6 @@ void replace_file(const fs::path& source, const fs::path& destination) {
#endif
if (ec) throw ThemeError(500, "THEME_SAVE_FAILED", "Could not save theme package", path_to_utf8(destination));
}
const std::set<std::string> kColors = {
"bg", "surface", "surface-alt", "surface-hi", "shell-hi", "shell-bg",
"border", "border-soft", "fg", "fg-2", "fg-mute", "accent", "accent-bg",
"accent-soft", "ok", "ok-bg", "ok-border", "warn", "warn-bg", "danger",
"danger-bg", "code-bg", "code-fg", "code-line", "selection", "on-selection",
"send-bg", "send-fg"
};

bool hex(const json& value, std::size_t length, bool prefix = false) {
if (!value.is_string()) return false;
const auto& text = value.get_ref<const std::string&>();
return text.size() == length && (!prefix || text[0] == '#') &&
std::all_of(text.begin() + (prefix ? 1 : 0), text.end(), [](unsigned char c) {
return (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F');
});
}

bool version_ok(const json& value) {
if (!value.is_string()) return false;
const auto& s = value.get_ref<const std::string&>();
return !s.empty() && s.size() <= 40 && s.front() >= '0' && s.front() <= '9' &&
std::all_of(s.begin(), s.end(), [](unsigned char c) {
return (c >= '0' && c <= '9') || c == '.' || c == '-' || (c >= 'a' && c <= 'z');
}) && s.find("..") == std::string::npos;
}

bool asset_ok(const json& asset, std::uintmax_t limit) {
return asset.is_object() && asset.contains("bytes") && asset["bytes"].is_number_unsigned() &&
asset["bytes"].get<std::uintmax_t>() > 0 && asset["bytes"].get<std::uintmax_t>() <= limit &&
asset.contains("sha256") && hex(asset["sha256"], 64);
}

std::string read_file(const fs::path& file, std::uintmax_t limit) {
std::error_code ec;
if (!fs::is_regular_file(file, ec) || ec || fs::file_size(file, ec) > limit || ec) {
Expand Down Expand Up @@ -199,10 +170,6 @@ bool matches_file(const fs::path& file, const json& expected) {
} catch (...) { return false; }
}

bool png(const std::string& bytes) {
return bytes.size() >= 24 && bytes.compare(0, 8, "\x89PNG\r\n\x1a\n", 8) == 0;
}

bool busy(const json& job) {
const auto state = job.value("state", "idle");
return state == "downloading" || state == "installing";
Expand All @@ -213,28 +180,6 @@ std::string theme_base(std::string update_base) {
return update_base + "/themes/";
}

void validate_theme_files(const json& definition, const ThemePackageFiles& files, bool decode_legacy_images = true) {
const auto images = theme_image_files(definition);
if (files.size() != images.size() + 1 || !files.count("theme.json"))
throw ThemeError(422, "THEME_INVALID_PACKAGE", "Theme resources must match the definition");
std::size_t total = files.at("theme.json").size();
for (const auto& [kind, name] : images) {
const auto found = files.find(name);
if (found == files.end()) throw ThemeError(422, "THEME_INVALID_PACKAGE", "Missing theme image: " + name);
const auto& bytes = found->second;
total += bytes.size();
image::ImageNormalizeOptions options;
options.force_png = true;
options.max_edge = 0;
options.final_max_bytes = kMaxPackageBytes;
if (total > kMaxPackageBytes || !png(bytes) || bytes.size() != definition.at(kind).at("bytes") ||
sha256_hex(bytes) != definition.at(kind).at("sha256") ||
((decode_legacy_images || (kind != "background" && kind != "thumbnail")) &&
!image::normalize_image_bytes(bytes, "image/png", options).ok))
throw ThemeError(422, "THEME_INVALID_PACKAGE", "Theme image integrity or size check failed");
}
}

std::map<std::string, std::string> unpack_archive(zip_t* archive) {
const auto count = archive ? zip_get_num_entries(archive, 0) : 0;
if (count < 3 || count > 5) {
Expand Down Expand Up @@ -308,109 +253,6 @@ std::map<std::string, std::string> unpack_import(const std::string& bytes) {
}
} // namespace

std::string theme_workshop_base(const std::string& update_base) {
std::unique_ptr<CURLU, decltype(&curl_url_cleanup)> url(curl_url(), curl_url_cleanup);
const auto invalid = [] { return ThemeError(400, "THEME_WORKSHOP_ADDRESS_INVALID", "Invalid workshop address in update configuration"); };
if (!url || update_base.find('\0') != std::string::npos ||
curl_url_set(url.get(), CURLUPART_URL, update_base.c_str(), CURLU_DISALLOW_USER) != CURLUE_OK) throw invalid();
const auto part = [&](CURLUPart key, unsigned flags = 0) {
char* raw = nullptr;
const auto status = curl_url_get(url.get(), key, &raw, flags);
std::unique_ptr<char, decltype(&curl_free)> value(raw, curl_free);
if (status != CURLUE_OK) throw invalid();
return std::string(value.get());
};
const auto scheme = part(CURLUPART_SCHEME);
if (scheme != "http" && scheme != "https") throw invalid();
const auto port = std::stoi(part(CURLUPART_PORT, CURLU_DEFAULT_PORT));
if (port < 1 || port > 55535) throw invalid();
if (curl_url_set(url.get(), CURLUPART_PORT, std::to_string(port + 10000).c_str(), 0) != CURLUE_OK ||
curl_url_set(url.get(), CURLUPART_PATH, "/", 0) != CURLUE_OK ||
curl_url_set(url.get(), CURLUPART_QUERY, nullptr, 0) != CURLUE_OK ||
curl_url_set(url.get(), CURLUPART_FRAGMENT, nullptr, 0) != CURLUE_OK) throw invalid();
return part(CURLUPART_URL);
}

ThemeError::ThemeError(int status, std::string code, const std::string& message, std::string path)
: std::runtime_error(message), status(status), code(std::move(code)), path(std::move(path)) {}

bool valid_theme_colors(const json& colors) {
try {
if (!colors.is_object() || colors.size() != kColors.size()) return false;
for (const auto& key : kColors) if (!hex(colors.at(key), 7, true)) return false;
return true;
} catch (...) { return false; }
}

bool valid_theme_appearance(const json& appearance) {
if (!appearance.is_object()) return false;
for (const auto& item : appearance.items()) {
if (item.key() == "logo_color" || item.key() == "home_title_color" ||
item.key() == "home_background_color" || item.key() == "session_background_color" ||
item.key() == "user_message_background_color") {
if (!hex(item.value(), 7, true)) return false;
} else if (item.key() == "extend_to_titlebar") {
if (!item.value().is_boolean()) return false;
} else if (item.key() == "home_composer_opacity" || item.key() == "home_background_opacity" ||
item.key() == "session_background_opacity" || item.key() == "user_message_background_opacity") {
if (!item.value().is_number()) return false;
const auto opacity = item.value().get<double>();
if (!(opacity >= 0.0 && opacity <= 1.0)) return false;
} else return false;
}
return true;
}

std::map<std::string, std::string> theme_image_files(const json& definition) {
std::map<std::string, std::string> result = {{"background", "background.png"}, {"thumbnail", "thumbnail.png"}};
if (definition.contains("session_background")) result.emplace("session_background", "session-background.png");
if (definition.contains("user_message_background")) result.emplace("user_message_background", "user-message-background.png");
return result;
}

bool valid_theme_definition(const json& d) {
try {
const auto id = d.at("id").get<std::string>();
const bool local = is_local_theme(id);
if (d.at("schema_version") != 1 || (!is_downloadable_theme(id) && !local) ||
!version_ok(d.at("version")) ||
(d.at("mode") != "light" && (id == "eva-01" || d.at("mode") != "dark")) ||
!valid_theme_colors(d.at("colors")) ||
(d.contains("appearance") && !valid_theme_appearance(d.at("appearance"))) ||
!asset_ok(d.at("background"), kMaxPackageBytes) ||
!asset_ok(d.at("thumbnail"), kMaxPreviewBytes)) return false;
for (const auto* kind : {"session_background", "user_message_background"})
if (d.contains(kind) && !asset_ok(d.at(kind), kMaxPackageBytes)) return false;
if (local || id == kNationalDayThemeId) {
const auto name = d.at("name").get<std::string>();
if (name.empty() || name.size() > 256 ||
name.find_first_not_of(" \t\r\n") == std::string::npos ||
std::any_of(name.begin(), name.end(), [](unsigned char c) { return c < 32 || c == 127; })) return false;
}
return true;
} catch (...) { return false; }
}

bool valid_theme_catalog(const json& catalog) {
try {
if (catalog.at("schema_version") != 1 || !catalog.at("themes").is_array() ||
catalog.at("themes").empty() || catalog.at("themes").size() > 2) return false;
std::set<std::string> ids;
for (const auto& e : catalog.at("themes")) {
const auto id = e.at("id").get<std::string>();
if (!is_downloadable_theme(id) || !ids.insert(id).second || !version_ok(e.at("version")) ||
!asset_ok(e.at("package"), kMaxPackageBytes) ||
!asset_ok(e.at("thumbnail"), kMaxPreviewBytes) ||
!e.at("swatches").is_array() || e.at("swatches").size() != 3) return false;
const auto prefix = id + "/" + e.at("version").get<std::string>() + "/";
if (e.at("package").at("path") != prefix + "theme.zip" ||
e.at("thumbnail").at("path") != prefix + "thumbnail.png") return false;
for (const auto& color : e.at("swatches")) if (!hex(color, 7, true)) return false;
}
return true;
} catch (...) { return false; }
}

ThemeStore::ThemeStore(fs::path root, std::string update_base, ThemeTransport transport)
: ThemeStore(std::move(root), [update_base = std::move(update_base)] { return update_base; },
std::move(transport)) {}
Expand Down
Loading
Loading