Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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: 0 additions & 2 deletions README.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ Codex Mate is a local-first CLI + Web UI for unified management of:
- Claude Code profiles (writes to `~/.claude/settings.json`)
- OpenClaw JSON5 profiles and workspace `AGENTS.md`
- Local Codex/Claude sessions (list/filter/export/delete)
- `codexmate qwen` passthrough command

It works on local files directly and does not require cloud hosting.

Expand Down Expand Up @@ -58,7 +57,6 @@ It works on local files directly and does not require cloud hosting.
- MCP stdio domains (`tools`, `resources`, `prompts`)
- Built-in proxy controls (`proxy`)
- Auth profile management (`auth`)
- `codexmate qwen` compatibility entrypoint (passthrough with `--yolo`)
- Zip/unzip utilities

## Architecture
Expand Down
2 changes: 0 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ Codex Mate 提供一套本地优先的 CLI + Web UI,用于统一管理:
- Claude Code 配置方案(写入 `~/.claude/settings.json`)
- OpenClaw JSON5 配置与 Workspace `AGENTS.md`
- Codex / Claude 本地会话浏览、筛选、导出、删除
- `codexmate qwen` 快捷透传命令

项目不依赖云端托管,配置写入你的本地文件,便于审计和回滚。

Expand Down Expand Up @@ -58,7 +57,6 @@ Codex Mate 提供一套本地优先的 CLI + Web UI,用于统一管理:
- MCP stdio 能力(tools/resources/prompts)
- 内建代理配置与状态控制(`proxy`)
- 认证档案管理(`auth`)
- `codexmate qwen` 兼容启动入口(默认透传 `--yolo`)
- Zip 压缩/解压(优先系统工具,失败回退 JS 库)

## 架构总览
Expand Down
1 change: 0 additions & 1 deletion site/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ Codex Mate 是一个本地优先的配置与会话管理工具,覆盖:
- Claude Code 配置方案管理(写入 `~/.claude/settings.json`)
- OpenClaw JSON5 配置与 Workspace `AGENTS.md`
- Codex / Claude 本地会话浏览、导出、删除
- `codexmate qwen` 透传入口

## 快速开始

Expand Down
40 changes: 40 additions & 0 deletions tests/unit/compact-layout-ui.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import assert from 'assert';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const projectRoot = path.resolve(__dirname, '..', '..');

function readProjectFile(relativePath) {
return fs.readFileSync(path.join(projectRoot, relativePath), 'utf8');
}

test('app script includes compact layout detection and body class toggling', () => {
const appScript = readProjectFile('web-ui/app.js');
assert.match(appScript, /forceCompactLayout:\s*false/);
assert.match(appScript, /updateCompactLayoutMode\(\)/);
assert.match(appScript, /shouldForceCompactLayout\(\)/);
assert.match(appScript, /applyCompactLayoutClass\(enabled\)/);
assert.match(appScript, /shouldForceCompactLayoutMode\(\{/);
assert.match(appScript, /classList\.toggle\('force-compact'/);
});

test('styles include force-compact fallback rules for readability on touch devices', () => {
const styles = readProjectFile('web-ui/styles.css');
assert.match(styles, /\.card-trailing\s*\{[\s\S]*align-items:\s*start;[\s\S]*align-self:\s*flex-start;/);
assert.match(styles, /\.card-trailing\s+\.card-actions\s*\{[\s\S]*justify-self:\s*end;/);
assert.match(styles, /\.card-trailing\s+\.pill,\s*[\s\S]*justify-self:\s*end;/);
assert.match(styles, /body\.force-compact\s*\{/);
assert.match(styles, /body\.force-compact\s+\.app-shell\s*\{/);
assert.match(styles, /body\.force-compact\s+\.status-inspector\s*\{[\s\S]*display:\s*none;/);
assert.match(styles, /body\.force-compact\s+\.top-tabs\s*\{[\s\S]*display:\s*grid\s*!important;/);
assert.match(styles, /body\.force-compact\s+\.card-subtitle/);
assert.match(styles, /body\.force-compact\s+\.provider-fast-switch\s*\{/);
assert.match(styles, /body\.force-compact\s+\.card\s*\{[\s\S]*flex-direction:\s*column;/);
assert.match(styles, /body\.force-compact\s+\.card-trailing\s*\{[\s\S]*justify-items:\s*end;/);
assert.match(styles, /body\.force-compact\s+\.card-trailing\s+\.card-actions\s*\{[\s\S]*justify-content:\s*flex-end;/);
assert.match(styles, /body\.force-compact\s+\.card-trailing\s+\.pill,\s*[\s\S]*justify-self:\s*end;/);
assert.match(styles, /body\.force-compact\s+\.card-actions\s*\{[\s\S]*opacity:\s*1;/);
});
8 changes: 8 additions & 0 deletions tests/unit/config-tabs-ui.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ test('config template keeps expected config tabs in top and side navigation', ()
assert.deepStrictEqual(sideTabModes, ['codex', 'claude', 'openclaw']);
assert.match(html, /activeProviderBridgeHint/);
assert.match(html, /isProviderConfigMode/);
assert.match(html, /provider-fast-switch-select/);
assert.match(html, /forceCompactLayout/);
assert.match(html, /quickSwitchProvider\(\$event\.target\.value\)/);
});

test('web ui script defines provider mode metadata for codex only', () => {
Expand All @@ -34,6 +37,11 @@ test('web ui script defines provider mode metadata for codex only', () => {
assert.match(appScript, /\.\.\.createConfigModeComputed\(\)/);
assert.match(appScript, /switchConfigMode\(mode\)/);
assert.match(appScript, /mode\.trim\(\)\.toLowerCase\(\)/);
assert.match(appScript, /quickSwitchProvider\(name\)/);
assert.match(appScript, /performProviderSwitch\(name\)/);
assert.match(appScript, /runLatestOnlyQueue\(/);
assert.match(appScript, /providerSwitchInProgress:\s*false/);
assert.match(appScript, /pendingProviderSwitch:\s*''/);

assert.match(configModeComputed, /const PROVIDER_CONFIG_MODE_META = Object\.freeze\(/);
const providerModeKeys = [...configModeComputed.matchAll(/^\s*([a-z]+):\s*Object\.freeze\(/gm)]
Expand Down
1 change: 1 addition & 0 deletions tests/unit/run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ await import(pathToFileURL(path.join(__dirname, 'mcp-stdio.test.mjs')));
await import(pathToFileURL(path.join(__dirname, 'workflow-engine.test.mjs')));
await import(pathToFileURL(path.join(__dirname, 'skills-modal-ui.test.mjs')));
await import(pathToFileURL(path.join(__dirname, 'config-tabs-ui.test.mjs')));
await import(pathToFileURL(path.join(__dirname, 'compact-layout-ui.test.mjs')));
await import(pathToFileURL(path.join(__dirname, 'unzip-ext.test.mjs')));
await import(pathToFileURL(path.join(__dirname, 'codex-proxy-options.test.mjs')));

Expand Down
111 changes: 110 additions & 1 deletion tests/unit/web-ui-logic.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ const {
buildSessionFilterCacheState,
buildSessionListParams,
formatSessionTimelineTimestamp,
buildSessionTimelineNodes
buildSessionTimelineNodes,
runLatestOnlyQueue,
shouldForceCompactLayoutMode
} = logic;

test('normalizeClaudeValue trims strings and ignores non-string', () => {
Expand Down Expand Up @@ -122,6 +124,113 @@ test('buildSpeedTestIssue maps errors and status codes', () => {
assert.strictEqual(http400.code, 'remote-speedtest-http-error');
});

test('runLatestOnlyQueue drains pending targets in order', async () => {
let pending = '';
const visited = [];
const result = await runLatestOnlyQueue('alpha', {
perform: async (target) => {
visited.push(target);
if (target === 'alpha') pending = 'beta';
if (target === 'beta') pending = 'gamma';
},
consumePending: () => {
const next = pending;
pending = '';
return next;
}
});
assert.deepStrictEqual(visited, ['alpha', 'beta', 'gamma']);
assert.strictEqual(result.lastTarget, 'gamma');
assert.strictEqual(result.lastError, '');
});

test('runLatestOnlyQueue continues after failure and preserves latest error', async () => {
let pending = '';
const visited = [];
const result = await runLatestOnlyQueue('alpha', {
perform: async (target) => {
visited.push(target);
if (target === 'alpha') {
pending = 'beta';
throw new Error('alpha failed');
}
},
consumePending: () => {
const next = pending;
pending = '';
return next;
}
});
assert.deepStrictEqual(visited, ['alpha', 'beta']);
assert.strictEqual(result.lastTarget, 'beta');
assert.strictEqual(result.lastError, 'alpha failed');
});

test('shouldForceCompactLayoutMode keeps desktop layout for narrow non-touch windows', () => {
const enabled = shouldForceCompactLayoutMode({
viewportWidth: 840,
screenWidth: 1920,
screenHeight: 1080,
maxTouchPoints: 0,
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
coarsePointer: false,
noHover: false
});
assert.strictEqual(enabled, false);
});

test('shouldForceCompactLayoutMode enables compact mode for mobile UA on narrow viewport', () => {
const enabled = shouldForceCompactLayoutMode({
viewportWidth: 390,
screenWidth: 390,
screenHeight: 844,
maxTouchPoints: 5,
userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) Mobile',
coarsePointer: true,
noHover: true
});
assert.strictEqual(enabled, true);
});

test('shouldForceCompactLayoutMode enables compact mode for phone-like touch device with desktop UA', () => {
const enabled = shouldForceCompactLayoutMode({
viewportWidth: 430,
screenWidth: 430,
screenHeight: 932,
maxTouchPoints: 5,
userAgent: 'Mozilla/5.0 (X11; Linux x86_64)',
coarsePointer: true,
noHover: true
});
assert.strictEqual(enabled, true);
});

test('shouldForceCompactLayoutMode keeps desktop layout for touch laptop with large screen', () => {
const enabled = shouldForceCompactLayoutMode({
viewportWidth: 1366,
screenWidth: 1366,
screenHeight: 1024,
maxTouchPoints: 10,
userAgent: 'Mozilla/5.0 (Windows NT 10.0; Win64; x64)',
coarsePointer: false,
noHover: false
});
assert.strictEqual(enabled, false);
});

test('shouldForceCompactLayoutMode requires touch points for non-mobile UA compact fallback', () => {
const enabled = shouldForceCompactLayoutMode({
viewportWidth: 768,
screenWidth: 768,
screenHeight: 1024,
maxTouchPoints: 0,
userAgent: 'Mozilla/5.0 (X11; Linux x86_64)',
coarsePointer: true,
noHover: true
});
assert.strictEqual(enabled, false);
});

test('isSessionQueryEnabled supports codex/claude/all', () => {
assert.strictEqual(isSessionQueryEnabled('codex'), true);
assert.strictEqual(isSessionQueryEnabled('CODEX'), true);
Expand Down
Loading
Loading