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
30 changes: 16 additions & 14 deletions src-tauri/src/cli/i18n.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5647,9 +5647,17 @@ pub mod texts {

pub fn tui_skills_sync_method_title() -> &'static str {
if is_chinese() {
"选择同步方式"
"选择 Skills 同步方式"
} else {
"Select Sync Method"
"Select Skill Sync Method"
}
}

pub fn tui_settings_skills_sync_method_label() -> &'static str {
if is_chinese() {
"Skills 同步方式"
} else {
"Skill Sync Method"
}
}

Expand Down Expand Up @@ -5702,25 +5710,19 @@ pub mod texts {

pub fn tui_skills_sync_method_name(method: crate::services::skill::SyncMethod) -> &'static str {
match method {
crate::services::skill::SyncMethod::Auto => {
if is_chinese() {
"自动(优先使用链接,失败时复制)"
} else {
"Automatic (prefer links, fall back to copy)"
}
}
crate::services::skill::SyncMethod::Symlink => {
crate::services::skill::SyncMethod::Auto
| crate::services::skill::SyncMethod::Symlink => {
if is_chinese() {
"仅链接"
"软连接"
} else {
"Links only"
"Symlink"
}
}
crate::services::skill::SyncMethod::Copy => {
if is_chinese() {
"仅复制"
"文件复制"
} else {
"Copy only"
"Copy Files"
}
}
}
Expand Down
4 changes: 3 additions & 1 deletion src-tauri/src/cli/tui/app/app_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,7 @@ pub enum SettingsItem {
VisibleAppsMode,
VisibleApps,
SkillsStorageLocation,
SkillsSyncMethod,
OpenClawConfigDir,
ManagedAccounts,
SkipClaudeOnboarding,
Expand All @@ -516,7 +517,7 @@ pub enum SettingsItem {
}

impl SettingsItem {
pub const ALL: [SettingsItem; 16] = [
pub const ALL: [SettingsItem; 17] = [
SettingsItem::ManagedAccounts,
SettingsItem::Language,
SettingsItem::Theme,
Expand All @@ -525,6 +526,7 @@ impl SettingsItem {
SettingsItem::VisibleAppsMode,
SettingsItem::VisibleApps,
SettingsItem::SkillsStorageLocation,
SettingsItem::SkillsSyncMethod,
SettingsItem::OpenClawConfigDir,
SettingsItem::SkipClaudeOnboarding,
SettingsItem::ClaudePluginIntegration,
Expand Down
6 changes: 6 additions & 0 deletions src-tauri/src/cli/tui/app/content_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -937,6 +937,12 @@ impl App {
};
Action::None
}
Some(SettingsItem::SkillsSyncMethod) => {
self.overlay = Overlay::SkillsSyncMethodPicker {
selected: sync_method_picker_index(data.skills.sync_method),
};
Action::None
}
Some(SettingsItem::OpenClawConfigDir) => {
let buffer = crate::settings::get_settings()
.openclaw_config_dir
Expand Down
12 changes: 4 additions & 8 deletions src-tauri/src/cli/tui/app/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1785,21 +1785,17 @@ pub(crate) fn snippet_picker_app_type(index: usize) -> AppType {
app_type_for_picker_index(index)
}

#[cfg(test)]
#[allow(dead_code)]
pub(crate) fn sync_method_picker_index(method: SyncMethod) -> usize {
match method {
SyncMethod::Auto => 0,
SyncMethod::Symlink => 1,
SyncMethod::Copy => 2,
SyncMethod::Auto | SyncMethod::Symlink => 0,
SyncMethod::Copy => 1,
}
}

pub(crate) fn sync_method_for_picker_index(index: usize) -> SyncMethod {
match index {
1 => SyncMethod::Symlink,
2 => SyncMethod::Copy,
_ => SyncMethod::Auto,
1 => SyncMethod::Copy,
_ => SyncMethod::Symlink,
}
}

Expand Down
2 changes: 1 addition & 1 deletion src-tauri/src/cli/tui/app/overlay_handlers/pickers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,7 +271,7 @@ impl App {
Action::None
}
KeyCode::Down => {
*selected = (*selected + 1).min(4);
*selected = (*selected + 1).min(1);
Action::None
}
KeyCode::Enter => {
Expand Down
65 changes: 65 additions & 0 deletions src-tauri/src/cli/tui/app/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10947,6 +10947,71 @@ mod tests {
));
}

#[test]
#[serial(home_settings)]
fn settings_skills_sync_method_uses_the_upstream_two_choice_picker() {
let temp_home = TempDir::new().expect("create temp home");
let _env = TestEnvGuard::isolated(temp_home.path());

let mut app = App::new(Some(AppType::Claude));
app.route = Route::Settings;
app.focus = Focus::Content;
app.settings_idx = SettingsItem::ALL
.iter()
.position(|item| matches!(item, SettingsItem::SkillsSyncMethod))
.expect("SkillsSyncMethod missing from SettingsItem::ALL");

let mut data = UiData::default();
data.skills.sync_method = crate::services::skill::SyncMethod::Auto;

{
let _lang = use_test_language(Language::Chinese);
let help = crate::cli::tui::help::context_help_for_app(&app, &data);
let body = help.lines.join("\n");
assert_eq!(help.title, "Skills 同步方式");
assert!(body.contains("选择 Skills 的文件同步策略"), "{body}");
assert!(body.contains("软连接节省磁盘空间并支持实时同步"), "{body}");
}

assert!(matches!(
app.on_key(key(KeyCode::Enter), &data),
Action::None
));
assert!(matches!(
app.overlay,
Overlay::SkillsSyncMethodPicker { selected: 0 }
));

assert!(matches!(
app.on_key(key(KeyCode::Enter), &data),
Action::SkillsSetSyncMethod {
method: crate::services::skill::SyncMethod::Symlink
}
));
assert!(matches!(app.overlay, Overlay::None));

data.skills.sync_method = crate::services::skill::SyncMethod::Symlink;
app.overlay = Overlay::SkillsSyncMethodPicker { selected: 0 };
assert!(matches!(
app.on_key(key(KeyCode::Down), &data),
Action::None
));
assert!(matches!(
app.on_key(key(KeyCode::Down), &data),
Action::None
));
assert!(matches!(
app.overlay,
Overlay::SkillsSyncMethodPicker { selected: 1 }
));
assert!(matches!(
app.on_key(key(KeyCode::Enter), &data),
Action::SkillsSetSyncMethod {
method: crate::services::skill::SyncMethod::Copy
}
));
}

#[test]
#[serial(home_settings)]
fn visible_apps_picker_rejects_zero_selection_without_closing() {
Expand Down
10 changes: 10 additions & 0 deletions src-tauri/src/cli/tui/help.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ enum HelpTarget {
FailoverQueue,
PreferredEditor,
SkillStorageLocation,
SkillSyncMethod,
GlobalOutboundProxy,
CodexOfficialAuthPreservation,
CodexUnifiedSessionHistory,
Expand Down Expand Up @@ -113,6 +114,7 @@ fn current_help_target(app: &App) -> HelpTarget {
provider_local_proxy_overlay_target(app, LocalProxySettingsField::UserAgent)
}
Overlay::ExternalEditorPicker { .. } => HelpTarget::PreferredEditor,
Overlay::SkillsSyncMethodPicker { .. } => HelpTarget::SkillSyncMethod,
Overlay::SkillsStorageLocationPicker { .. } => HelpTarget::SkillStorageLocation,
Overlay::ClaudeModelPicker { .. } => {
provider_field_overlay_target(app, ProviderAddField::ClaudeModelConfig)
Expand Down Expand Up @@ -160,6 +162,7 @@ fn current_help_target(app: &App) -> HelpTarget {
match SettingsItem::ALL.get(app.settings_idx) {
Some(SettingsItem::PreferredEditor) => return HelpTarget::PreferredEditor,
Some(SettingsItem::SkillsStorageLocation) => return HelpTarget::SkillStorageLocation,
Some(SettingsItem::SkillsSyncMethod) => return HelpTarget::SkillSyncMethod,
Some(SettingsItem::OutboundProxy) => return HelpTarget::GlobalOutboundProxy,
Some(SettingsItem::PreserveCodexOfficialAuth) => {
return HelpTarget::CodexOfficialAuthPreservation;
Expand Down Expand Up @@ -346,6 +349,13 @@ fn help_for_target(target: HelpTarget, app: &App, data: &UiData) -> HelpContent
"Choose either CC Switch's managed directory or the shared ~/.agents/skills directory as the single source of truth for managed Skills. Switching moves only Skills recorded in the database and refreshes links or copies for enabled apps; unmanaged directories are not imported, claimed, or moved. If Unified contains unmanaged directories, cloud sync refuses to replace the root; import them explicitly or switch back to CC Switch storage first.\nMigration stops and keeps the current setting when the target already has a same-named directory, even with identical content; only an interrupted copy carrying this migration's receipt can resume automatically. If app refresh is partial, the old copy is retained; select the current location again to retry reconciliation. Do not run another Skill install or update command while migration is active.",
),
),
HelpTarget::SkillSyncMethod => HelpContent::new(
texts::tui_settings_skills_sync_method_label(),
help_lines(
"选择 Skills 的文件同步策略。软连接节省磁盘空间并支持实时同步;文件复制适用于不支持软连接的环境。Windows 使用软连接时可能需要管理员权限或开启开发者模式。\n此设置只影响之后的 Skills 部署或同步,不会迁移主存储位置,也不会立即重建现有应用目录。如需立即应用,请在 Skills 页面执行同步。",
"Choose how to sync Skills files. Symlinks save disk space and enable real-time sync; copying files supports environments where symlinks are unavailable. On Windows, symlinks may require administrator privileges or Developer Mode.\nThis setting affects future Skill deployments or syncs only. It does not move the managed storage location or immediately rebuild existing app directories. Run Sync from the Skills page to apply it now.",
),
),
HelpTarget::GlobalOutboundProxy => HelpContent::new(
crate::t!("Global Outbound Proxy", "全局出站代理"),
help_lines(
Expand Down
5 changes: 5 additions & 0 deletions src-tauri/src/cli/tui/ui/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ fn settings_section(item: SettingsItem) -> SettingsSection {
SettingsItem::VisibleAppsMode
| SettingsItem::VisibleApps
| SettingsItem::SkillsStorageLocation
| SettingsItem::SkillsSyncMethod
| SettingsItem::OpenClawConfigDir => SettingsSection::Applications,
SettingsItem::SkipClaudeOnboarding
| SettingsItem::ClaudePluginIntegration
Expand Down Expand Up @@ -3487,6 +3488,10 @@ pub(super) fn render_settings(
)
.to_string(),
),
super::app::SettingsItem::SkillsSyncMethod => (
texts::tui_settings_skills_sync_method_label().to_string(),
texts::tui_skills_sync_method_name(data.skills.sync_method).to_string(),
),
super::app::SettingsItem::OpenClawConfigDir => (
texts::tui_settings_openclaw_config_dir_label().to_string(),
openclaw_config_dir.clone().unwrap_or_else(|| {
Expand Down
10 changes: 5 additions & 5 deletions src-tauri/src/cli/tui/ui/overlay/pickers.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use super::super::theme;
use super::super::*;
use super::frame::{overlay_frame, overlay_frame_at, OverlaySize};
use crate::cli::tui::app::sync_method_picker_index;
use crate::cli::tui::form;
use crate::cli::tui::form::{ClaudeModelRole, HermesModelField, ProviderAddFormState};
use crate::cli::tui::text_edit::TextInput;
Expand Down Expand Up @@ -2464,7 +2465,6 @@ pub(super) fn render_skills_sync_method_picker_overlay(
selected: usize,
) {
let methods = [
crate::services::skill::SyncMethod::Auto,
crate::services::skill::SyncMethod::Symlink,
crate::services::skill::SyncMethod::Copy,
];
Expand All @@ -2475,7 +2475,7 @@ pub(super) fn render_skills_sync_method_picker_overlay(
theme,
texts::tui_skills_sync_method_title(),
&[
("←→", texts::tui_key_select()),
("↑↓", texts::tui_key_select()),
("Enter", texts::tui_key_apply()),
("Esc", texts::tui_key_cancel()),
],
Expand All @@ -2486,10 +2486,10 @@ pub(super) fn render_skills_sync_method_picker_overlay(
overlay_border_style(theme, false),
);

let current = data.skills.sync_method;
let current = sync_method_picker_index(data.skills.sync_method);

let items = methods.into_iter().map(|method| {
let marker = if method == current {
let items = methods.into_iter().enumerate().map(|(index, method)| {
let marker = if index == current {
texts::tui_marker_active()
} else {
texts::tui_marker_inactive()
Expand Down
28 changes: 27 additions & 1 deletion src-tauri/src/cli/tui/ui/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4966,6 +4966,7 @@ fn settings_page_groups_items_with_unlabeled_dividers() {
&content,
texts::tui_settings_skills_storage_location_label(),
);
let skill_sync = line_index(&content, texts::tui_settings_skills_sync_method_label());
let openclaw_dir = line_index(&content, texts::tui_settings_openclaw_config_dir_label());
let claude_integration = line_index(&content, texts::enable_claude_plugin_integration_label());
let codex_login = line_index(&content, texts::codex_preserve_official_auth_label());
Expand All @@ -4980,7 +4981,8 @@ fn settings_page_groups_items_with_unlabeled_dividers() {
assert!(
dividers[0] < visible_apps
&& visible_apps < skill_storage
&& skill_storage < openclaw_dir
&& skill_storage < skill_sync
&& skill_sync < openclaw_dir
&& openclaw_dir < dividers[1],
"{content}"
);
Expand All @@ -5004,6 +5006,30 @@ fn settings_page_groups_items_with_unlabeled_dividers() {
}
}

#[test]
fn settings_skill_sync_method_matches_upstream_labels_and_choices() {
let _lock = lock_env();
let _lang = use_test_language(Language::English);
let _no_color = EnvGuard::remove("NO_COLOR");

let mut app = App::new(Some(AppType::Claude));
app.route = Route::Settings;
app.focus = Focus::Content;

let mut data = minimal_data(&app.app_type);
data.skills.sync_method = SyncMethod::Auto;
let buf = render_with_size(&app, &data, 100, 32);
let content = content_text(&app, &buf);
assert!(content.contains("Skill Sync Method"), "{content}");
assert!(content.contains("Symlink"), "{content}");

app.overlay = Overlay::SkillsSyncMethodPicker { selected: 0 };
let overlay = all_text(&render_with_size(&app, &data, 100, 32));
assert!(overlay.contains("Symlink"), "{overlay}");
assert!(overlay.contains("Copy Files"), "{overlay}");
assert!(!overlay.contains("Automatic"), "{overlay}");
}

#[test]
fn settings_section_rows_do_not_shift_selection_or_break_narrow_scrolling() {
let _lock = lock_env();
Expand Down
Loading