Skip to content

Commit 0e38488

Browse files
authored
refactor(web-ui): brand redesign and time format simplification v0.0.36 (#173)
* refactor(web-ui): brand block minimalist redesign per Apple Glass aesthetic - Reduce logo from 38px to 28px with 6px border radius - Simplify title: 18px/700 → 13px/500 with gradient text - Remove subtitle entirely for cleaner layout - Version number now fades in on hover only - Replace warm border with subtle 0.5px black alpha - Add inner glow and radial hover effect on logo - Refine padding and spacing for breathing room * fix(web-ui): update precompiled render function for brand block changes * test(parity): allow brandHovered as extra data key * test(parity): allow brandHovered in both parity branches * feat(web-ui): add relative time display with i18n support - Add translation keys for relative time (zh/en/ja) - Modify formatSessionTimelineTimestamp to support relative time - Pass t function to buildUsageHeatmap and buildSessionTimelineNodes - Add updatedAtLabel field to session list items - Update panel-sessions.html to use formatted time labels * fix(web-ui): add t parameter to buildUsageChartGroups * fix(web-ui): add error handling for relative time formatting * refactor(web-ui): simplify time format to absolute only - Remove relative time logic (just now, N minutes ago, etc.) - Use fixed format: YYYY-MM-DD HH:mm - Remove i18n keys for relative time - Update tests to match new format - Net code reduction: 91 lines * chore: bump version to 0.0.36 * fix(web-ui): guard session shape before accessing updatedAt * fix(web-ui): keyboard accessibility and gradient fallback - Add tabindex and focus/blur handlers to brand-block for keyboard navigation - Add focus-visible outline styles for keyboard users - Wrap gradient text in @supports for browser fallback - Update precompiled render function --------- Co-authored-by: ymkiux <ymkiux@users.noreply.github.com>
1 parent 9ffba03 commit 0e38488

12 files changed

Lines changed: 138 additions & 77 deletions

cli.js

Lines changed: 21 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -4879,27 +4879,29 @@ function listClaudeSessions(limit, options = {}) {
48794879
}
48804880
}
48814881

4882-
if (sessions.length === 0) {
4883-
const fallbackFiles = collectRecentJsonlFiles(claudeProjectsDir, {
4884-
returnCount: scanCount,
4885-
maxFilesScanned,
4886-
ignoreSubPath: `${path.sep}subagents${path.sep}`
4882+
// 补充扫描未索引的 .jsonl 文件(包括 sessions-index.json 中遗漏的会话)
4883+
const seenFilePaths = new Set(sessions.map((item) => item.filePath).filter(Boolean));
4884+
const fallbackFiles = collectRecentJsonlFiles(claudeProjectsDir, {
4885+
returnCount: scanCount,
4886+
maxFilesScanned,
4887+
ignoreSubPath: `${path.sep}subagents${path.sep}`
4888+
});
4889+
for (const filePath of fallbackFiles) {
4890+
if (seenFilePaths.has(filePath)) continue;
4891+
const summary = parseClaudeSessionSummary(filePath, {
4892+
summaryReadBytes,
4893+
titleReadBytes
48874894
});
4888-
for (const filePath of fallbackFiles) {
4889-
const summary = parseClaudeSessionSummary(filePath, {
4890-
summaryReadBytes,
4891-
titleReadBytes
4892-
});
4893-
if (summary) {
4894-
sessions.push(attachSessionNativeStatus({
4895-
...summary,
4896-
derived: isDerivedSessionFile(filePath)
4897-
}));
4898-
}
4895+
if (summary) {
4896+
sessions.push(attachSessionNativeStatus({
4897+
...summary,
4898+
derived: isDerivedSessionFile(filePath)
4899+
}));
4900+
seenFilePaths.add(filePath);
4901+
}
48994902

4900-
if (sessions.length >= targetCount) {
4901-
break;
4902-
}
4903+
if (sessions.length >= targetCount) {
4904+
break;
49034905
}
49044906
}
49054907

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "codexmate",
3-
"version": "0.0.35",
3+
"version": "0.0.36",
44
"description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具",
55
"main": "cli.js",
66
"bin": {

tests/unit/web-ui-behavior-parity.test.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,7 @@ test('captured bundled app skeleton only exposes expected data key drift versus
325325
const missingCurrentKeys = headDataKeys.filter((key) => !currentDataKeys.includes(key)).sort();
326326
const allowedExtraCurrentKeys = parityAgainstHead ? [
327327
'appVersion',
328+
'brandHovered',
328329
'sessionListInitialBatchSize',
329330
'sessionListLoadStep',
330331
'sessionListVisibleCount',
@@ -354,6 +355,7 @@ test('captured bundled app skeleton only exposes expected data key drift versus
354355
'showEditProviderKey'
355356
] : [
356357
'appVersion',
358+
'brandHovered',
357359
'__mainTabSwitchState',
358360
'openclawAuthProfilesByProvider',
359361
'openclawPendingAuthProfileUpdates',

tests/unit/web-ui-logic.test.mjs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -957,8 +957,8 @@ test('activeSessionVisibleMessages falls back to the initial preview batch befor
957957
});
958958

959959
test('formatSessionTimelineTimestamp normalizes ISO-like strings for timeline labels', () => {
960-
assert.strictEqual(formatSessionTimelineTimestamp('2026-03-23T09:10:11.000Z'), '03-23 09:10:11');
961-
assert.strictEqual(formatSessionTimelineTimestamp('2026-03-23 19:20:00'), '03-23 19:20:00');
960+
assert.strictEqual(formatSessionTimelineTimestamp('2026-03-23T09:10:11.000Z'), '2026-03-23 09:10');
961+
assert.strictEqual(formatSessionTimelineTimestamp('2026-03-23 19:20:00'), '2026-03-23 19:20');
962962
assert.strictEqual(formatSessionTimelineTimestamp('not-a-time'), 'not-a-time');
963963
assert.strictEqual(formatSessionTimelineTimestamp(''), '');
964964
});
@@ -978,8 +978,8 @@ test('buildSessionTimelineNodes builds per-message node metadata', () => {
978978
assert.strictEqual(nodes[0].key, 'user-0');
979979
assert.strictEqual(nodes[0].role, 'user');
980980
assert.strictEqual(nodes[0].roleShort, 'U');
981-
assert.strictEqual(nodes[0].displayTime, '03-23 09:00:00');
982-
assert.strictEqual(nodes[0].title, '#1 · User · 03-23 09:00:00');
981+
assert.strictEqual(nodes[0].displayTime, '2026-03-23 09:00');
982+
assert.strictEqual(nodes[0].title, '#1 · User · 2026-03-23 09:00');
983983
assert.strictEqual(nodes[0].percent, 0);
984984
assert.strictEqual(nodes[0].safePercent, 6);
985985

web-ui/app.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ document.addEventListener('DOMContentLoaded', () => {
3131
const appOptions = {
3232
data() {
3333
return {
34+
brandHovered: false,
3435
lang: 'zh',
3536
appVersion: '',
3637
mainTab: 'dashboard',

web-ui/logic.sessions.mjs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -180,12 +180,9 @@ export function formatSessionTimelineTimestamp(timestamp) {
180180
if (!value) return '';
181181

182182
const matched = value.match(/^(\d{4})-(\d{2})-(\d{2})[T\s](\d{2}):(\d{2})(?::(\d{2}))?/);
183-
if (matched) {
184-
const second = matched[6] || '00';
185-
return `${matched[2]}-${matched[3]} ${matched[4]}:${matched[5]}:${second}`;
186-
}
183+
if (!matched) return value;
187184

188-
return value;
185+
return `${matched[1]}-${matched[2]}-${matched[3]} ${matched[4]}:${matched[5]}`;
189186
}
190187

191188
function normalizeUsageRange(range) {

web-ui/modules/i18n.dict.mjs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -532,6 +532,7 @@ const DICT = Object.freeze({
532532
'sessions.loadingList': '会话加载中...',
533533
'sessions.empty': '暂无可用会话记录',
534534
'sessions.unknownTime': '未知时间',
535+
535536
'sessions.query.placeholder.enabled': '关键词检索(支持 Codex/Claude/Gemini/CodeBuddy,例:claude code)',
536537
'sessions.query.placeholder.disabled': '当前来源暂不支持关键词检索',
537538
'sessions.pin': '置顶',
@@ -1602,6 +1603,8 @@ const DICT = Object.freeze({
16021603
'sessions.loadingList': 'セッション一覧を読み込み中...',
16031604
'sessions.empty': 'セッションがありません',
16041605
'sessions.unknownTime': '不明な時間',
1606+
1607+
16051608
'sessions.query.placeholder.enabled': 'セッションを検索...',
16061609
'sessions.query.placeholder.disabled': '現在のソースでは検索は利用できません',
16071610
'sessions.pin': 'ピン留め',
@@ -2658,6 +2661,8 @@ const DICT = Object.freeze({
26582661
'sessions.loadingList': 'Loading sessions...',
26592662
'sessions.empty': 'No sessions found',
26602663
'sessions.unknownTime': 'unknown time',
2664+
2665+
26612666
'sessions.query.placeholder.enabled': 'Search keywords (Codex/Claude/Gemini/CodeBuddy, e.g. claude code)',
26622667
'sessions.query.placeholder.disabled': 'Keyword search is not available for this source',
26632668
'sessions.pin': 'Pin',

web-ui/partials/index/layout-header.html

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,14 +118,13 @@
118118

119119
<div :class="['app-shell', { standalone: sessionStandalone }]">
120120
<aside class="side-rail" v-if="!sessionStandalone">
121-
<div class="brand-block">
121+
<div class="brand-block" tabindex="0" @mouseenter="brandHovered = true" @mouseleave="brandHovered = false" @focus="brandHovered = true" @blur="brandHovered = false">
122122
<div class="brand-head">
123123
<img class="brand-logo" src="/res/logo-pack.webp" alt="Codex Mate logo">
124124
<div class="brand-copy">
125-
<div class="brand-kicker">Codex Mate <span v-if="appVersion" class="brand-version">v{{ appVersion }}</span></div>
125+
<div class="brand-kicker">Codex Mate<transition name="brand-version-fade"><span v-if="appVersion && brandHovered" class="brand-version"> v{{ appVersion }}</span></transition></div>
126126
</div>
127127
</div>
128-
<div class="brand-subtitle">{{ t('brand.subtitle.localConfigSessionsWorkspace') }}</div>
129128
</div>
130129

131130
<div class="side-rail-nav">

web-ui/partials/index/panel-sessions.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,7 @@
193193
</div>
194194
<div class="session-item-meta">
195195
<span class="session-source" :data-source="session.source">{{ session.sourceLabel }}</span>
196-
<span class="session-item-time">{{ session.updatedAt || t('sessions.unknownTime') }}</span>
196+
<span class="session-item-time">{{ session.updatedAtLabel || session.updatedAt || t('sessions.unknownTime') }}</span>
197197
<span v-if="getSessionHotLabel(session)" class="session-item-hot">{{ getSessionHotLabel(session) }}</span>
198198
<span v-if="session.cwd" class="session-item-cwd session-item-sub">{{ session.cwd }}</span>
199199
<div v-if="session.match && session.match.snippets && session.match.snippets.length" class="session-match-snippets">
@@ -212,7 +212,7 @@
212212
<div class="session-preview-title">{{ activeSession.title || activeSession.sessionId }}</div>
213213
<div class="session-preview-meta">
214214
<span class="session-preview-meta-item session-source" :data-source="activeSession.source">{{ activeSession.sourceLabel }}</span>
215-
<span class="session-preview-meta-item">{{ activeSession.updatedAt || t('sessions.unknownTime') }}</span>
215+
<span class="session-preview-meta-item">{{ activeSession.updatedAtLabel || activeSession.updatedAt || t('sessions.unknownTime') }}</span>
216216
</div>
217217
<div class="session-preview-meta" v-if="activeSession.cwd">
218218
<span class="session-preview-meta-item">{{ activeSession.cwd }}</span>

web-ui/res/web-ui-render.precompiled.js

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
window.__CODEXMATE_WEB_UI_RENDER__ = (() => {
2-
const { toDisplayString: _toDisplayString, normalizeClass: _normalizeClass, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock, createCommentVNode: _createCommentVNode, createTextVNode: _createTextVNode, Fragment: _Fragment, renderList: _renderList, vShow: _vShow, withDirectives: _withDirectives, vModelSelect: _vModelSelect, vModelText: _vModelText, withKeys: _withKeys, withModifiers: _withModifiers, isMemoSame: _isMemoSame, withMemo: _withMemo, normalizeStyle: _normalizeStyle, vModelCheckbox: _vModelCheckbox, vModelDynamic: _vModelDynamic } = Vue
2+
const { toDisplayString: _toDisplayString, normalizeClass: _normalizeClass, createElementVNode: _createElementVNode, openBlock: _openBlock, createElementBlock: _createElementBlock, createCommentVNode: _createCommentVNode, Transition: _Transition, withCtx: _withCtx, createVNode: _createVNode, createTextVNode: _createTextVNode, Fragment: _Fragment, renderList: _renderList, vShow: _vShow, withDirectives: _withDirectives, vModelSelect: _vModelSelect, vModelText: _vModelText, withKeys: _withKeys, withModifiers: _withModifiers, isMemoSame: _isMemoSame, withMemo: _withMemo, normalizeStyle: _normalizeStyle, vModelCheckbox: _vModelCheckbox, vModelDynamic: _vModelDynamic } = Vue
33

44
return function render(_ctx, _cache) {
55
return (_openBlock(), _createElementBlock(_Fragment, null, [
@@ -165,7 +165,14 @@ return function render(_ctx, _cache) {
165165
key: 0,
166166
class: "side-rail"
167167
}, [
168-
_createElementVNode("div", { class: "brand-block" }, [
168+
_createElementVNode("div", {
169+
class: "brand-block",
170+
tabindex: "0",
171+
onMouseenter: $event => (_ctx.brandHovered = true),
172+
onMouseleave: $event => (_ctx.brandHovered = false),
173+
onFocus: $event => (_ctx.brandHovered = true),
174+
onBlur: $event => (_ctx.brandHovered = false)
175+
}, [
169176
_createElementVNode("div", { class: "brand-head" }, [
170177
_createElementVNode("img", {
171178
class: "brand-logo",
@@ -174,18 +181,22 @@ return function render(_ctx, _cache) {
174181
}),
175182
_createElementVNode("div", { class: "brand-copy" }, [
176183
_createElementVNode("div", { class: "brand-kicker" }, [
177-
_createTextVNode("Codex Mate "),
178-
(_ctx.appVersion)
179-
? (_openBlock(), _createElementBlock("span", {
180-
key: 0,
181-
class: "brand-version"
182-
}, "v" + _toDisplayString(_ctx.appVersion), 1 /* TEXT */))
183-
: _createCommentVNode("v-if", true)
184+
_createTextVNode("Codex Mate"),
185+
_createVNode(_Transition, { name: "brand-version-fade" }, {
186+
default: _withCtx(() => [
187+
(_ctx.appVersion && _ctx.brandHovered)
188+
? (_openBlock(), _createElementBlock("span", {
189+
key: 0,
190+
class: "brand-version"
191+
}, " v" + _toDisplayString(_ctx.appVersion), 1 /* TEXT */))
192+
: _createCommentVNode("v-if", true)
193+
]),
194+
_: 1 /* STABLE */
195+
})
184196
])
185197
])
186-
]),
187-
_createElementVNode("div", { class: "brand-subtitle" }, _toDisplayString(_ctx.t('brand.subtitle.localConfigSessionsWorkspace')), 1 /* TEXT */)
188-
]),
198+
])
199+
], 40 /* PROPS, NEED_HYDRATION */, ["onMouseenter", "onMouseleave", "onFocus", "onBlur"]),
189200
_createElementVNode("div", { class: "side-rail-nav" }, [
190201
_createElementVNode("div", {
191202
class: "side-section",
@@ -2447,7 +2458,7 @@ return function render(_ctx, _cache) {
24472458
class: "session-source",
24482459
"data-source": session.source
24492460
}, _toDisplayString(session.sourceLabel), 9 /* TEXT, PROPS */, ["data-source"]),
2450-
_createElementVNode("span", { class: "session-item-time" }, _toDisplayString(session.updatedAt || _ctx.t('sessions.unknownTime')), 1 /* TEXT */),
2461+
_createElementVNode("span", { class: "session-item-time" }, _toDisplayString(session.updatedAtLabel || session.updatedAt || _ctx.t('sessions.unknownTime')), 1 /* TEXT */),
24512462
(_ctx.getSessionHotLabel(session))
24522463
? (_openBlock(), _createElementBlock("span", {
24532464
key: 0,
@@ -2505,7 +2516,7 @@ return function render(_ctx, _cache) {
25052516
class: "session-preview-meta-item session-source",
25062517
"data-source": _ctx.activeSession.source
25072518
}, _toDisplayString(_ctx.activeSession.sourceLabel), 9 /* TEXT, PROPS */, ["data-source"]),
2508-
_createElementVNode("span", { class: "session-preview-meta-item" }, _toDisplayString(_ctx.activeSession.updatedAt || _ctx.t('sessions.unknownTime')), 1 /* TEXT */)
2519+
_createElementVNode("span", { class: "session-preview-meta-item" }, _toDisplayString(_ctx.activeSession.updatedAtLabel || _ctx.activeSession.updatedAt || _ctx.t('sessions.unknownTime')), 1 /* TEXT */)
25092520
]),
25102521
(_ctx.activeSession.cwd)
25112522
? (_openBlock(), _createElementBlock("div", {

0 commit comments

Comments
 (0)