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
74 changes: 74 additions & 0 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -1000,6 +1000,65 @@ function normalizePromptsSubTabPreference(value) {
return normalized === 'claude-project' ? 'claude-project' : 'codex';
}

function normalizeSidebarCollapsedPreference(value) {
return normalizeBooleanPreference(value, false);
}

function normalizeSessionFilterSourcePreference(value) {
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
if (normalized === 'all' || normalized === 'claude' || normalized === 'gemini' || normalized === 'codebuddy') return normalized;
return 'codex';
}

function normalizeSessionRoleFilterPreference(value) {
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
if (normalized === 'user' || normalized === 'assistant' || normalized === 'system' || normalized === 'tool') return normalized;
return 'all';
}

function normalizeSessionTimePresetPreference(value) {
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
if (normalized === '24h' || normalized === '7d' || normalized === '30d') return normalized;
return 'all';
}

function normalizeSessionSortModePreference(value) {
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
return normalized === 'hot' ? 'hot' : 'time';
}

function normalizeSessionFiltersPreference(value = {}) {
const source = isPlainObject(value) ? value : {};
return {
source: normalizeSessionFilterSourcePreference(source.source),
pathFilter: typeof source.pathFilter === 'string' ? source.pathFilter : '',
query: typeof source.query === 'string' ? source.query : '',
roleFilter: normalizeSessionRoleFilterPreference(source.roleFilter),
timePreset: normalizeSessionTimePresetPreference(source.timePreset),
sortMode: normalizeSessionSortModePreference(source.sortMode)
};
}

function normalizeSessionPinnedMapPreference(value = {}) {
const source = isPlainObject(value) ? value : {};
const next = {};
for (const [key, item] of Object.entries(source)) {
if (!key) continue;
const numeric = Number(item);
if (!Number.isFinite(numeric) || numeric <= 0) continue;
next[key] = Math.floor(numeric);
}
return next;
}

function normalizePlainObjectPreference(value = {}) {
return isPlainObject(value) ? value : {};
}

function normalizeArrayPreference(value = []) {
return Array.isArray(value) ? value : [];
}

function normalizeWebUiPreferences(value = {}) {
const source = isPlainObject(value) ? value : {};
const navigation = isPlainObject(source.navigation) ? source.navigation : {};
Expand All @@ -1012,6 +1071,18 @@ function normalizeWebUiPreferences(value = {}) {
sessionsUsageTimeRange: normalizeUsageTimeRangePreference(source.sessionsUsageTimeRange),
promptsSubTab: normalizePromptsSubTabPreference(source.promptsSubTab),
projectClaudeMdPath: typeof source.projectClaudeMdPath === 'string' ? source.projectClaudeMdPath : '',
sidebarCollapsed: normalizeSidebarCollapsedPreference(source.sidebarCollapsed),
starPrompted: normalizeBooleanPreference(source.starPrompted, false),
taskOrchestrationTabEnabled: normalizeBooleanPreference(source.taskOrchestrationTabEnabled, true),
sessionLoadNativeDialog: normalizeBooleanPreference(source.sessionLoadNativeDialog, false),
language: typeof source.language === 'string' ? source.language : '',
sessionFilters: normalizeSessionFiltersPreference(source.sessionFilters),
sessionPinnedMap: normalizeSessionPinnedMapPreference(source.sessionPinnedMap),
claudeConfigs: normalizePlainObjectPreference(source.claudeConfigs),
currentClaudeConfig: typeof source.currentClaudeConfig === 'string' ? source.currentClaudeConfig : '',
openclawConfigs: normalizePlainObjectPreference(source.openclawConfigs),
toolConfigPermissions: normalizeToolConfigPermissions(source.toolConfigPermissions || TOOL_CONFIG_PERMISSION_DEFAULTS),
deletedClaudeSettingsImports: normalizeArrayPreference(source.deletedClaudeSettingsImports),
navigation: {
mainTab: normalizeMainTabPreference(navigation.mainTab),
configMode: normalizeConfigModePreference(navigation.configMode),
Expand Down Expand Up @@ -1040,6 +1111,9 @@ function setWebUiPreferences(params = {}) {
}
});
preferences.webUi = next;
if (isPlainObject(incoming.toolConfigPermissions)) {
preferences.toolConfigPermissions = normalizeToolConfigPermissions(incoming.toolConfigPermissions);
}
writeCodexmatePreferences(preferences);
return { success: true, preferences: next };
}
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codexmate",
"version": "0.0.56",
"version": "0.0.57",
"description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具",
"main": "cli.js",
"bin": {
Expand Down
61 changes: 53 additions & 8 deletions tests/e2e/test-web-ui-session-tab.js
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,10 @@ function createBundledNavigationContext(appOptions) {
vm.cancelSessionTimelineSync = function cancelSessionTimelineSync() {
this._cancelTimelineSyncCalls += 1;
};
vm._persistCalls = [];
vm.persistWebUiPreferences = function persistWebUiPreferences(overrides) {
this._persistCalls.push(overrides || {});
};

return vm;
}
Expand All @@ -113,6 +117,7 @@ module.exports = async function testWebUiSessionTab() {
globalThis.window = createWindowMock('http://127.0.0.1:3737/web-ui/index.html?tab=sessions#stale');
try {
vm.mainTab = 'sessions';
vm.preserveSessionRenderOnTabLeave = false;
vm.sessionListRenderEnabled = true;
vm.sessionPreviewRenderEnabled = true;
vm.sessionTabRenderTicket = 5;
Expand All @@ -128,8 +133,8 @@ module.exports = async function testWebUiSessionTab() {

assert(vm.fastHidden === true, 'pointerdown should hide the sessions panel immediately');
assert(
String(globalThis.localStorage.getItem('codexmateNavState.v1') || '').includes('"mainTab":"settings"'),
'pointerdown should persist the intended nav tab even while the commit is deferred'
vm._persistCalls.some((call) => call && call.navigation && call.navigation.mainTab === 'settings'),
'pointerdown should persist the intended nav tab through web UI preferences even while the commit is deferred'
);
assert(vm.mainTab === 'sessions', 'tab commit should stay deferred until the next frame');
assert(vm.sessionListRenderEnabled === false, 'leaving sessions should suspend list rendering immediately');
Expand Down Expand Up @@ -163,14 +168,54 @@ module.exports = async function testWebUiSessionTab() {
assert(vm.sessionListRenderEnabled === true, 'session list rendering should recover after canceling the leave');
assert(vm.sessionPreviewRenderEnabled === true, 'session preview rendering should recover after canceling the leave');

globalThis.localStorage.setItem('codexmateNavState.v1', JSON.stringify({
mainTab: 'usage',
configMode: 'codex'
}));
const vm2 = createBundledNavigationContext(appOptions);
vm2.mainTab = 'dashboard';
vm2.restoreNavStateFromStorage();
assert(vm2.mainTab === 'usage', 'nav state restore should select the cached sidebar tab');
vm2.applyWebUiPreferences({
navigation: {
mainTab: 'usage',
configMode: 'codex'
}
});
assert(vm2.mainTab === 'usage', 'preference navigation restore should select the cached sidebar tab');

const vm3 = createBundledNavigationContext(appOptions);
vm3.mainTab = 'usage';
vm3.applyWebUiPreferences({
navigation: {
mainTab: 'orchestration',
configMode: 'codex'
}
});
assert(vm3.mainTab === 'dashboard', 'preference navigation restore should fall back when the task tab is disabled');

vm3.switchMainTab('orchestration');
assert(vm3.mainTab === 'dashboard', 'programmatic task tab selection should fall back to the first selectable tab');

vm3.mainTab = 'usage';
vm3.switchMainTab('');
assert(vm3.mainTab === 'usage', 'empty tab selection should be ignored');

vm3.switchMainTab('not-a-tab');
assert(vm3.mainTab === 'usage', 'unknown tab selection should be ignored');

let prevented = 0;
let stopped = 0;
vm3.mainTab = 'usage';
vm3.onMainTabPointerDown('orchestration', {
button: 0,
pointerType: 'mouse',
preventDefault() { prevented += 1; },
stopPropagation() { stopped += 1; }
});
assert(vm3.mainTab === 'usage', 'pointerdown should not select the disabled task tab');
assert(prevented === 1 && stopped === 1, 'disabled task tab pointerdown should cancel the event');

vm3.onMainTabClick('orchestration', {
preventDefault() { prevented += 1; },
stopPropagation() { stopped += 1; }
});
assert(vm3.mainTab === 'usage', 'click should not select the disabled task tab');
assert(prevented === 2 && stopped === 2, 'disabled task tab click should cancel the event');
} finally {
globalThis.localStorage = prevLocalStorage;
globalThis.window = prevWindow;
Expand Down
Loading
Loading