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
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 0.9.12 LANGUAGES C CXX)
project(acecode VERSION 0.9.13 LANGUAGES C CXX)

option(ACECODE_TUI_INPUT_TRACE "Enable verbose TUI input event trace logging." OFF)

Expand Down Expand Up @@ -95,7 +95,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 98 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
10 changes: 10 additions & 0 deletions src/config/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -992,6 +992,14 @@ static AppConfig load_config_from_path_once(
LOG_WARN("[config] invalid 'web_ui.font_size', using 'medium'");
}
}
if (uij.contains("sidebar_session_time")) {
if (uij["sidebar_session_time"].is_boolean()) {
cfg.web_ui.sidebar_session_time =
uij["sidebar_session_time"].get<bool>();
} else {
LOG_WARN("[config] invalid 'web_ui.sidebar_session_time', using true");
}
}
}
}
if (j.contains("models_dev") && j["models_dev"].is_object()) {
Expand Down Expand Up @@ -2123,6 +2131,8 @@ nlohmann::json build_config_json(const AppConfig& cfg) {
web_uij["color_theme"] = cfg.web_ui.color_theme;
if (cfg.web_ui.font_size != web_ui_d.font_size)
web_uij["font_size"] = cfg.web_ui.font_size;
if (cfg.web_ui.sidebar_session_time != web_ui_d.sidebar_session_time)
web_uij["sidebar_session_time"] = cfg.web_ui.sidebar_session_time;
if (!web_uij.empty()) j["web_ui"] = std::move(web_uij);

MemoryConfig mem_d;
Expand Down
3 changes: 3 additions & 0 deletions src/config/config.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,9 @@ struct WebUiPreferencesConfig {
std::string theme = "system"; // system | light | dark
std::string color_theme = "blue"; // blue | orange
std::string font_size = "medium"; // small | medium | large
// Sidebar session rows show a relative timestamp. Product default is on;
// turning it off leaves the time visible only in the row hover card.
bool sidebar_session_time = true;
};

struct ModelsDevConfig {
Expand Down
1 change: 1 addition & 0 deletions src/desktop/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2162,6 +2162,7 @@ int main(int argc, char** argv) {
{"theme", desktop_cfg.web_ui.theme},
{"color_theme", desktop_cfg.web_ui.color_theme},
{"font_size", desktop_cfg.web_ui.font_size},
{"sidebar_session_time", desktop_cfg.web_ui.sidebar_session_time},
}.dump();
const std::string startup_bootstrap = startup_timeline.snapshot_json();
host.init_script(acecode::desktop::locale_bootstrap_script(
Expand Down
11 changes: 11 additions & 0 deletions src/web/routes/routes_misc.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2249,6 +2249,13 @@ void WebServer::Impl::register_ui_preferences() {
"font_size must be small, medium, or large");
}
}
if (body.contains("sidebar_session_time")) {
has_supported_field = true;
if (!body["sidebar_session_time"].is_boolean()) {
return json_err(400, "BAD_REQUEST",
"sidebar_session_time must be a boolean");
}
}
if (!has_supported_field) {
return json_err(400, "BAD_REQUEST",
"no supported UI preference field was provided");
Expand All @@ -2269,6 +2276,10 @@ void WebServer::Impl::register_ui_preferences() {
deps.app_config->web_ui.font_size =
body["font_size"].get<std::string>();
}
if (body.contains("sidebar_session_time")) {
deps.app_config->web_ui.sidebar_session_time =
body["sidebar_session_time"].get<bool>();
}
try {
if (!deps.config_path.empty()) {
save_config(*deps.app_config, deps.config_path);
Expand Down
1 change: 1 addition & 0 deletions src/web/server_helpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ json ui_preferences_to_json(const WebUiPreferencesConfig& cfg) {
{"theme", cfg.theme},
{"color_theme", cfg.color_theme},
{"font_size", cfg.font_size},
{"sidebar_session_time", cfg.sidebar_session_time},
};
}

Expand Down
68 changes: 68 additions & 0 deletions tests/config/config_web_ui_preferences_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,74 @@ TEST(ConfigWebUiPreferencesSave, NonDefaultAppearanceRoundTrips) {
std::filesystem::remove(path, ec);
}

// 场景:产品对「侧栏是否显示任务时间」有分歧,做成可配置项。期望:默认开,
// 只有显式 false 才关,非布尔值保留默认,默认值不写进 config.json(稀疏落盘
// 约定),非默认值能完整往返。
TEST(ConfigWebUiPreferencesSidebarSessionTime, DefaultsToShown) {
WebUiPreferencesConfig prefs;
EXPECT_TRUE(prefs.sidebar_session_time);

AppConfig cfg;
EXPECT_TRUE(cfg.web_ui.sidebar_session_time);
}

TEST(ConfigWebUiPreferencesSidebarSessionTime, ExplicitFalseLoads) {
const auto path = temp_config_path("session-time-false");
write_json(path, nlohmann::json{
{"web_ui", {{"sidebar_session_time", false}}},
});

AppConfig cfg = load_config_from_path(path.string());
EXPECT_FALSE(cfg.web_ui.sidebar_session_time);

std::error_code ec;
std::filesystem::remove(path, ec);
}

TEST(ConfigWebUiPreferencesSidebarSessionTime, NonBooleanKeepsDefault) {
const auto path = temp_config_path("session-time-invalid");
write_json(path, nlohmann::json{
{"web_ui", {{"sidebar_session_time", "no"}}},
});

AppConfig cfg = load_config_from_path(path.string());
EXPECT_TRUE(cfg.web_ui.sidebar_session_time);

std::error_code ec;
std::filesystem::remove(path, ec);
}

TEST(ConfigWebUiPreferencesSidebarSessionTime, OnlyNonDefaultIsPersisted) {
const auto path = temp_config_path("session-time-roundtrip");
std::error_code ec;
std::filesystem::remove(path, ec);

AppConfig cfg;
// 默认值不落盘:整个 web_ui 块应当仍然缺席。
save_config(cfg, path.string());
{
std::ifstream ifs(path);
ASSERT_TRUE(ifs.is_open());
const auto saved = nlohmann::json::parse(ifs);
EXPECT_FALSE(saved.contains("web_ui"));
}

cfg.web_ui.sidebar_session_time = false;
save_config(cfg, path.string());
{
std::ifstream ifs(path);
ASSERT_TRUE(ifs.is_open());
const auto saved = nlohmann::json::parse(ifs);
ASSERT_TRUE(saved.contains("web_ui"));
EXPECT_EQ(saved["web_ui"]["sidebar_session_time"], false);
}

AppConfig loaded = load_config_from_path(path.string());
EXPECT_FALSE(loaded.web_ui.sidebar_session_time);

std::filesystem::remove(path, ec);
}

TEST(ConfigWebUiPreferencesValidation, AcceptsOnlyCanonicalValues) {
EXPECT_TRUE(is_valid_web_ui_theme("system"));
EXPECT_TRUE(is_valid_web_ui_theme("light"));
Expand Down
38 changes: 38 additions & 0 deletions tests/web/web_server_smoke_test.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -7126,6 +7126,44 @@ TEST(WebServerHttp, PutUiPreferencesPersistsCompleteAppearance) {
EXPECT_EQ(saved.web_ui.font_size, "large");
}

// 场景:侧栏任务时间开关经 ui-preferences 往返。期望:GET 默认返回 true;
// PUT false 时内存与磁盘同步落到 false,且不碰其它外观字段;非布尔值 400 拒绝。
// 该字段对旧前端是纯增量,所以只做类型校验、不引入枚举白名单。
TEST(WebServerHttp, UiPreferencesSidebarSessionTimeRoundTrips) {
WebServerFixture fx;
{
auto get = cpr::Get(cpr::Url{fx.url("/api/config/ui-preferences")});
ASSERT_EQ(get.status_code, 200) << get.text;
EXPECT_EQ(json::parse(get.text)["sidebar_session_time"], true);
}

json req = {{"sidebar_session_time", false}};
auto put = cpr::Put(cpr::Url{fx.url("/api/config/ui-preferences")},
cpr::Header{{"Content-Type", "application/json"}},
cpr::Body{req.dump()});
ASSERT_EQ(put.status_code, 200) << put.text;
EXPECT_EQ(json::parse(put.text)["sidebar_session_time"], false);
EXPECT_FALSE(fx.cfg.web_ui.sidebar_session_time);
// 单字段 PUT 不能顺手把其它外观字段冲回默认。
EXPECT_EQ(fx.cfg.web_ui.theme, "system");
EXPECT_EQ(fx.cfg.web_ui.color_theme, "blue");
EXPECT_EQ(fx.cfg.web_ui.font_size, "medium");

const auto saved = acecode::load_config_from_path(
(fx.tmp_dir / "config.json").string());
EXPECT_FALSE(saved.web_ui.sidebar_session_time);
}

TEST(WebServerHttp, UiPreferencesSidebarSessionTimeRejectsNonBoolean) {
WebServerFixture fx;
json req = {{"sidebar_session_time", "no"}};
auto put = cpr::Put(cpr::Url{fx.url("/api/config/ui-preferences")},
cpr::Header{{"Content-Type", "application/json"}},
cpr::Body{req.dump()});
EXPECT_EQ(put.status_code, 400) << put.text;
EXPECT_TRUE(fx.cfg.web_ui.sidebar_session_time);
}

TEST(WebServerHttp, PutUiPreferencesPartialUpdatePreservesOtherAppearanceFields) {
WebServerFixture fx;
fx.server->with_app_config_lock([&] {
Expand Down
36 changes: 18 additions & 18 deletions vcpkg.json
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
{
"name": "acecode",
"version-semver": "0.9.12",
"dependencies": [
"cpr",
"crow",
"ftxui",
"libzip",
"nlohmann-json",
"sqlite3"
],
"features": {
"tests": {
"description": "Dependencies required only by ACECode unit tests",
"dependencies": [
"gtest"
]
}
}
"name": "acecode",
"version-semver": "0.9.13",
"dependencies": [
"cpr",
"crow",
"ftxui",
"libzip",
"nlohmann-json",
"sqlite3"
],
"features": {
"tests": {
"description": "Dependencies required only by ACECode unit tests",
"dependencies": [
"gtest"
]
}
}
}
12 changes: 11 additions & 1 deletion web/src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ import {
import {
DEFAULT_UI_PREFS,
effectiveFontSize,
effectiveSidebarSessionTime,
effectiveSidePanelListCollapsed,
UI_PREFS_STORAGE_KEY,
validateUiPrefs,
Expand Down Expand Up @@ -262,6 +263,7 @@ export function App() {
const initialUiPrefs = useMemo(() => ({
...DEFAULT_UI_PREFS,
fontSize: initialAppearance.fontSize,
sidebarSessionTime: initialAppearance.sidebarSessionTime,
}), [initialAppearance]);
const [uiPrefs, setUiPrefs] = usePreference(
UI_PREFS_STORAGE_KEY, initialUiPrefs, validateUiPrefs);
Expand Down Expand Up @@ -290,10 +292,14 @@ export function App() {
// grid4/grid9 入口暂时隐藏:主界面固定单会话,避免旧 localStorage 把用户卡在未完善视图。
const view = 'single';
const fontSize = effectiveFontSize(uiPrefs);
const sidebarSessionTime = effectiveSidebarSessionTime(uiPrefs);
const applyAppearance = useCallback((next) => {
setTheme(effectiveAppearanceTheme(next.theme));
setColorTheme(next.colorTheme);
setUiPrefs({ fontSize: next.fontSize });
setUiPrefs({
fontSize: next.fontSize,
sidebarSessionTime: next.sidebarSessionTime,
});
}, [setColorTheme, setTheme, setUiPrefs]);
const appearanceControllerRef = useRef(null);
if (!appearanceControllerRef.current) {
Expand All @@ -302,6 +308,7 @@ export function App() {
theme: bootstrapAppearance?.theme || theme,
colorTheme,
fontSize,
sidebarSessionTime,
},
apply: applyAppearance,
save: (payload) => api.setUiPreferences(payload),
Expand Down Expand Up @@ -2016,6 +2023,7 @@ export function App() {
onOpenExpertComponents={openExpertComponents}
pendingPermissionSessionIds={pendingPermissionSessionIdsForSidebar}
pendingQuestionSessionIds={pendingQuestionSessionIdsForSidebar}
showSessionTime={sidebarSessionTime}
/>
{view === 'single' && !sidebarCollapsed && (
<div
Expand Down Expand Up @@ -2128,6 +2136,8 @@ export function App() {
changeAppearance({ colorTheme: nextColorTheme })
)}
onFontSizeChange={(nextFontSize) => changeAppearance({ fontSize: nextFontSize })}
sidebarSessionTime={sidebarSessionTime}
onSidebarSessionTimeChange={(next) => changeAppearance({ sidebarSessionTime: next })}
/>
)}
<SearchPalette
Expand Down
18 changes: 18 additions & 0 deletions web/src/components/SettingsPage.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ export function SettingsPage({
onThemeChange,
onColorThemeChange,
onFontSizeChange = () => {},
sidebarSessionTime = true,
onSidebarSessionTimeChange = () => {},
}) {
const {
theme,
Expand Down Expand Up @@ -282,6 +284,8 @@ export function SettingsPage({
setColorTheme={setColorTheme}
fontSize={fontSize}
onFontSizeChange={onFontSizeChange}
sidebarSessionTime={sidebarSessionTime}
onSidebarSessionTimeChange={onSidebarSessionTimeChange}
/>
)}
{activeNavKey === 'config' && <SectionConfig />}
Expand Down Expand Up @@ -1162,6 +1166,8 @@ function SectionAppearance({
setColorTheme,
fontSize,
onFontSizeChange,
sidebarSessionTime,
onSidebarSessionTimeChange,
}) {
return (
<>
Expand Down Expand Up @@ -1230,6 +1236,18 @@ function SectionAppearance({
);
})}
</div>
<div className="h-px bg-border my-5" />
<div className="text-[14px] font-semibold mb-1">侧边栏</div>
<div className="flex items-center justify-between px-3.5 py-2.5 rounded-md bg-surface border border-border mb-2 max-w-md">
<div>
<div className="text-[13px] font-medium">显示任务时间</div>
<div className="text-[11px] text-fg-mute mt-0.5">在任务列表每一行右侧显示最近活动时间,关闭后仍可在悬停卡片里查看</div>
</div>
<Toggle
on={sidebarSessionTime}
onChange={(enabled) => onSidebarSessionTimeChange(enabled)}
/>
</div>
</>
);
}
Expand Down
Loading
Loading