From e34e97175b177baf2d426fa993ddb05a3f741265 Mon Sep 17 00:00:00 2001 From: ymkiux Date: Sat, 23 May 2026 21:47:22 +0800 Subject: [PATCH 01/24] chore: revert version to 0.0.34 after npm unpublish --- package.json | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/package.json b/package.json index 086cb09f..722a9e2b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codexmate", - "version": "0.1.0", + "version": "0.0.34", "description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具", "main": "cli.js", "bin": { @@ -31,18 +31,7 @@ "release:npm": "node tools/release/publish-npm.js", "docs:dev": "node ./node_modules/vitepress/dist/node/cli.js dev site", "docs:build": "node ./node_modules/vitepress/dist/node/cli.js build site", - "docs:preview": "node ./node_modules/vitepress/dist/node/cli.js preview site", - "ci:install": "node tools/ci/run-check.js install", - "ci:lint": "node tools/ci/run-check.js lint", - "ci:test": "node tools/ci/run-check.js test", - "lint": "node tools/dev/lint.js", - "test": "npm run test:unit && npm run test:e2e", - "test:ci": "node tools/ci/run-check.js all", - "test:unit": "node tests/unit/run.mjs", - "test:e2e": "node tests/e2e/run.js", - "setup:git": "git remote set-url origin https://github.com/SakuraByteCore/codexmate.git && gh auth setup-git", - "reset:dev": "node tools/dev/reset-and-dev.js", - "pretest": "node tools/ci/ensure-test-deps.js" + "docs:preview": "node ./node_modules/vitepress/dist/node/cli.js preview site" }, "dependencies": { "@iarna/toml": "^2.2.5", From 4b116bb8f0418d674cc84d0991d9a71c47c57c5f Mon Sep 17 00:00:00 2001 From: ymkiux Date: Sat, 23 May 2026 22:13:23 +0800 Subject: [PATCH 02/24] refactor(web-ui): redesign Usage tab with "River of Time" aesthetic - Merge current session bar with hero metrics for unified focus - Replace bar chart with SVG bezier curve wave visualization - Add automatic delta calculation (7d vs prior 7d, 30d vs prior 30d) - Redesign empty states with animated SVG illustrations - Simplify list views with bullet-point compact style - Enhance color system with delta indicators and heatmap levels --- web-ui/logic.sessions.mjs | 4 + web-ui/modules/app.computed.session.mjs | 153 +++++- web-ui/partials/index/panel-usage.html | 183 ++++--- web-ui/styles/base-theme.css | 10 + web-ui/styles/sessions-usage.css | 655 ++++++++++++------------ 5 files changed, 612 insertions(+), 393 deletions(-) diff --git a/web-ui/logic.sessions.mjs b/web-ui/logic.sessions.mjs index 929078f0..1187780d 100644 --- a/web-ui/logic.sessions.mjs +++ b/web-ui/logic.sessions.mjs @@ -549,11 +549,15 @@ export function buildUsageChartGroups(sessions = [], options = {}) { String(messageCount), String(sessionIndex) ].join(':'), + sessionId: session.sessionId || '', + filePath: session.filePath || '', title: normalizedTitle, source, sourceLabel, cwd, messageCount, + totalTokens: sessionTotalTokens, + contextWindow: sessionContextWindow, updatedAt: session.updatedAt || '', updatedAtMs, updatedAtLabel: formatSessionTimelineTimestamp(session.updatedAt || ''), diff --git a/web-ui/modules/app.computed.session.mjs b/web-ui/modules/app.computed.session.mjs index 571daaf3..215449c1 100644 --- a/web-ui/modules/app.computed.session.mjs +++ b/web-ui/modules/app.computed.session.mjs @@ -602,11 +602,10 @@ export function createSessionComputed() { const sessions = this.sessionUsageCharts && Array.isArray(this.sessionUsageCharts.filteredSessions) ? this.sessionUsageCharts.filteredSessions : this.sessionsUsageList; - const compareEnabled = this.sessionsUsageCompareEnabled === true && this.sessionsUsageTimeRange !== 'all'; const rangeDays = this.sessionsUsageTimeRange === '30d' ? 30 : 7; const dayMs = 24 * 60 * 60 * 1000; const baseMs = Date.parse(`${dayKey}T00:00:00.000Z`); - const prevKey = compareEnabled && Number.isFinite(baseMs) + const prevKey = Number.isFinite(baseMs) ? new Date(baseMs - (rangeDays * dayMs)).toISOString().slice(0, 10) : ''; let sessionCount = 0; @@ -636,7 +635,7 @@ export function createSessionComputed() { } else if (isPrev) { prevTokenTotal += sessionTokens; } - const model = typeof session.model === 'string' ? session.model.trim() : ''; + const model = typeof session.model === 'string' ? session.model.trim() : ''; if (isCurrent && model) { modelMap.set(model, (modelMap.get(model) || 0) + 1); } @@ -667,20 +666,162 @@ export function createSessionComputed() { })); return { dayKey, - compareEnabled, prevKey, sessionCount, messageCount, tokenTotal, tokenLabel: formatUsageSummaryNumber(tokenTotal), prevTokenTotal, - prevTokenLabel: compareEnabled ? formatUsageSummaryNumber(prevTokenTotal) : '0', - deltaTokenLabel: compareEnabled ? formatSignedUsageSummaryNumber(tokenTotal - prevTokenTotal) : '0', + prevTokenLabel: prevKey ? formatUsageSummaryNumber(prevTokenTotal) : null, + deltaTokenLabel: prevKey ? formatSignedUsageSummaryNumber(tokenTotal - prevTokenTotal) : null, topSessions, topModels }; }, + usageHeroMainValue() { + const summary = this.sessionUsageCharts && this.sessionUsageCharts.summary + ? this.sessionUsageCharts.summary + : null; + if (!summary) return '0'; + return formatCompactUsageSummaryNumber(summary.totalTokens || 0); + }, + + usageHeroSubLabel() { + const summary = this.sessionUsageCharts && this.sessionUsageCharts.summary + ? this.sessionUsageCharts.summary + : null; + if (!summary) return ''; + const t = typeof this.t === 'function' ? this.t : null; + const sessionCount = summary.totalSessions || 0; + const rangeLabel = this.sessionsUsageTimeRange === '30d' ? '30天' : (this.sessionsUsageTimeRange === 'all' ? '全部' : '7天'); + const rangeText = t ? t('usage.range.' + this.sessionsUsageTimeRange) : rangeLabel; + return `${formatUsageSummaryNumber(sessionCount)} sessions · ${rangeText}`; + }, + + usageHeroDelta() { + const range = this.sessionsUsageTimeRange; + if (range === 'all') return null; + const summary = this.sessionUsageCharts && this.sessionUsageCharts.summary + ? this.sessionUsageCharts.summary + : null; + if (!summary || !summary.totalTokens) return null; + + const rangeDays = range === '30d' ? 30 : 7; + const dayMs = 24 * 60 * 60 * 1000; + const nowMs = Date.now(); + const prevStartMs = nowMs - (rangeDays * 2 * dayMs); + const prevEndMs = nowMs - (rangeDays * dayMs); + + let prevTokens = 0; + for (const session of (Array.isArray(this.sessionsUsageList) ? this.sessionsUsageList : [])) { + if (!session || typeof session !== 'object') continue; + const updatedAtMs = Date.parse(session.updatedAt || ''); + if (!Number.isFinite(updatedAtMs)) continue; + if (updatedAtMs >= prevStartMs && updatedAtMs < prevEndMs) { + const sessionTokens = Number.isFinite(Number(session.totalTokens)) + ? Math.max(0, Math.floor(Number(session.totalTokens))) + : 0; + prevTokens += sessionTokens; + } + } + + if (prevTokens === 0) return null; + const currentTokens = summary.totalTokens; + const delta = currentTokens - prevTokens; + const deltaPercent = prevTokens > 0 ? Math.round((delta / prevTokens) * 100) : 0; + const arrow = delta > 0 ? '↑' : (delta < 0 ? '↓' : '–'); + const sign = delta >= 0 ? '+' : ''; + return `${arrow} ${sign}${deltaPercent}%`; + }, + + usageHeroDeltaClass() { + const summary = this.sessionUsageCharts && this.sessionUsageCharts.summary + ? this.sessionUsageCharts.summary + : null; + if (!summary || !summary.totalTokens) return ''; + + const range = this.sessionsUsageTimeRange; + if (range === 'all') return ''; + + const rangeDays = range === '30d' ? 30 : 7; + const dayMs = 24 * 60 * 60 * 1000; + const nowMs = Date.now(); + const prevStartMs = nowMs - (rangeDays * 2 * dayMs); + const prevEndMs = nowMs - (rangeDays * dayMs); + + let prevTokens = 0; + for (const session of (Array.isArray(this.sessionsUsageList) ? this.sessionsUsageList : [])) { + if (!session || typeof session !== 'object') continue; + const updatedAtMs = Date.parse(session.updatedAt || ''); + if (!Number.isFinite(updatedAtMs)) continue; + if (updatedAtMs >= prevStartMs && updatedAtMs < prevEndMs) { + const sessionTokens = Number.isFinite(Number(session.totalTokens)) + ? Math.max(0, Math.floor(Number(session.totalTokens))) + : 0; + prevTokens += sessionTokens; + } + } + + if (prevTokens === 0) return ''; + const currentTokens = summary.totalTokens; + return currentTokens >= prevTokens ? 'delta-up' : 'delta-down'; + }, + + sessionsUsageSelectedDay() { + return this.sessionsUsageSelectedDayKey || ''; + }, + + sessionUsageWave() { + const daily = this.sessionUsageDaily && typeof this.sessionUsageDaily === 'object' + ? this.sessionUsageDaily + : null; + if (!daily || !Array.isArray(daily.rows) || daily.rows.length === 0) { + return { points: [], labels: [], linePath: '', areaPath: '', width: 800, maxTokens: 0 }; + } + + const rows = daily.rows; + const maxTokens = daily.maxTokens || 1; + const width = 800; + const height = 140; + const padding = { top: 10, bottom: 30, left: 0, right: 0 }; + const chartWidth = width - padding.left - padding.right; + const chartHeight = height - padding.top - padding.bottom; + + const points = rows.map((row, index) => { + const x = padding.left + (index / (rows.length - 1 || 1)) * chartWidth; + const normalizedValue = maxTokens > 0 ? (row.tokenTotal / maxTokens) : 0; + const y = padding.top + chartHeight - (normalizedValue * chartHeight); + return { x, y, key: row.key, value: row.tokenTotal, label: row.label }; + }); + + const linePath = points.length > 1 + ? `M ${points.map(p => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(' L ')}` + : ''; + + const areaPath = points.length > 1 + ? `${linePath} L ${points[points.length - 1].x.toFixed(1)},${(padding.top + chartHeight).toFixed(1)} L ${points[0].x.toFixed(1)},${(padding.top + chartHeight).toFixed(1)} Z` + : ''; + + const selectedKey = this.sessionsUsageSelectedDayKey; + const selectedPoint = points.find(p => p.key === selectedKey) || points[points.length - 1] || null; + + return { + points, + labels: rows.map((row, index) => ({ + key: row.key, + text: row.label + })), + linePath, + areaPath, + width, + height, + maxTokens, + hoverX: selectedPoint ? selectedPoint.x : 0, + hoverY: selectedPoint ? selectedPoint.y : 0 + }; + }, + visibleSessionTrashItems() { const items = Array.isArray(this.sessionTrashItems) ? this.sessionTrashItems : []; const visibleCount = Number(this.sessionTrashVisibleCount); diff --git a/web-ui/partials/index/panel-usage.html b/web-ui/partials/index/panel-usage.html index 21e6e65a..8be5180a 100644 --- a/web-ui/partials/index/panel-usage.html +++ b/web-ui/partials/index/panel-usage.html @@ -1,4 +1,4 @@ - +
{{ t('usage.range.7d') }} -
-
{{ t('usage.loading') }}
-
{{ sessionsUsageError }}
-
{{ t('usage.empty') }}
+
+
+ + + + + +
+

{{ t('usage.loading') }}

+
+
+
+ + + +
+

{{ sessionsUsageError }}

+
+
+
+ + + + +
+

{{ t('usage.empty') }}

+
- \ No newline at end of file + diff --git a/web-ui/styles/base-theme.css b/web-ui/styles/base-theme.css index 62b8c688..819f7cd9 100644 --- a/web-ui/styles/base-theme.css +++ b/web-ui/styles/base-theme.css @@ -26,6 +26,16 @@ --color-success: #4B8B6A; --color-error: #C44536; + /* Delta 指示色 */ + --color-delta-up: #4caf50; + --color-delta-down: #f44336; + + /* 热力图色阶 */ + --color-heatmap-1: rgba(200, 121, 99, 0.2); + --color-heatmap-2: rgba(200, 121, 99, 0.4); + --color-heatmap-3: rgba(200, 121, 99, 0.6); + --color-heatmap-4: rgba(200, 121, 99, 0.85); + --bg-warm-gradient: radial-gradient(circle at 14% 8%, rgba(255, 219, 196, 0.5) 0%, rgba(255, 219, 196, 0) 32%), radial-gradient(circle at 88% 0%, rgba(252, 239, 207, 0.58) 0%, rgba(252, 239, 207, 0) 30%), diff --git a/web-ui/styles/sessions-usage.css b/web-ui/styles/sessions-usage.css index 8ae22a9b..8807756c 100644 --- a/web-ui/styles/sessions-usage.css +++ b/web-ui/styles/sessions-usage.css @@ -1,5 +1,5 @@ /* ============================================ - Usage Tab — Refined Design System + Usage Tab — 时光之河设计 ============================================ */ /* ---- Toolbar ---- */ @@ -9,21 +9,21 @@ align-items: center; gap: 12px; flex-wrap: wrap; - margin-bottom: 18px; + margin-bottom: 20px; } .usage-toolbar-title { - font-size: var(--font-size-title); - font-weight: var(--font-weight-title); + font-size: 18px; + font-weight: 600; color: var(--color-text-primary); - letter-spacing: -0.01em; + letter-spacing: -0.02em; } .usage-range-group { display: flex; - gap: 4px; - padding: 3px; - border-radius: var(--radius-md); + gap: 3px; + padding: 4px; + border-radius: 10px; background: var(--color-surface-alt); border: 1px solid var(--color-border-soft); } @@ -32,14 +32,14 @@ border: none; background: transparent; color: var(--color-text-tertiary); - padding: 5px 11px; - border-radius: var(--radius-sm); + padding: 6px 14px; + border-radius: 7px; cursor: pointer; - font-size: 12px; - font-weight: 600; + font-size: 13px; + font-weight: 500; font-family: var(--font-family); white-space: nowrap; - transition: all 120ms var(--ease-spring); + transition: all 150ms var(--ease-spring); outline: none; -webkit-tap-highlight-color: transparent; } @@ -50,13 +50,13 @@ } .usage-range-btn:active:not(:disabled) { - transform: scale(0.97); + transform: scale(0.96); } .usage-range-btn.active { color: #fff; background: var(--color-brand); - box-shadow: 0 1px 3px rgba(200, 121, 99, 0.35); + box-shadow: 0 2px 8px rgba(200, 121, 99, 0.3); } .usage-range-btn:disabled { @@ -65,34 +65,75 @@ } .usage-range-btn-icon { - padding: 5px 8px; + padding: 6px 10px; display: inline-flex; align-items: center; justify-content: center; } .usage-range-btn-icon svg { - width: 14px; - height: 14px; + width: 15px; + height: 15px; } -/* ---- Current session bar ---- */ -.usage-current-session-bar { +/* ---- Empty State (插画风格) ---- */ +.usage-empty-state { display: flex; - flex-wrap: wrap; + flex-direction: column; align-items: center; - gap: 10px; - padding: 8px 14px; + justify-content: center; + padding: 48px 20px; + text-align: center; +} + +.usage-empty-illustration { + width: 64px; + height: 64px; + color: var(--color-text-muted); + opacity: 0.6; margin-bottom: 16px; - border-radius: var(--radius-md); + animation: usage-empty-breathe 3s ease-in-out infinite; +} + +@keyframes usage-empty-breathe { + 0%, 100% { transform: scale(1); opacity: 0.5; } + 50% { transform: scale(1.05); opacity: 0.7; } +} + +.usage-empty-text { + font-size: 13px; + color: var(--color-text-secondary); + max-width: 280px; +} + +/* ---- Hero 区域 ---- */ +.usage-hero { + padding: 24px; + margin-bottom: 20px; + border-radius: 14px; + background: var(--color-surface); + border: 1px solid var(--color-border-soft); + min-height: 140px; + display: flex; + flex-direction: column; + justify-content: center; + gap: 16px; +} + +.usage-hero-active { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px 12px; + padding: 10px 16px; + border-radius: 10px; background: var(--color-surface-alt); font-size: 12px; - color: var(--color-text-secondary); } -.usage-current-session-dot { - width: 7px; - height: 7px; +.usage-hero-active-dot { + width: 6px; + height: 6px; border-radius: 50%; background: var(--color-success); flex-shrink: 0; @@ -100,258 +141,136 @@ } @keyframes usage-pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.4; } + 0%, 100% { opacity: 1; transform: scale(1); } + 50% { opacity: 0.4; transform: scale(0.9); } } -.usage-current-session-label { - font-weight: 700; +.usage-hero-active-label { + font-weight: 600; color: var(--color-text-primary); } -.usage-current-session-stat { +.usage-hero-active-stat { color: var(--color-text-tertiary); } -.usage-current-session-stat::before { +.usage-hero-active-stat::before { content: '·'; margin-right: 10px; color: var(--color-border-strong); } -/* ---- Summary cards ---- */ -.usage-summary-grid { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); - gap: 10px; - margin-bottom: 18px; -} - -.usage-summary-card { - display: flex; - flex-direction: column; - justify-content: center; - gap: 2px; - padding: 14px 16px; - border-radius: var(--radius-md); - background: var(--color-surface); - border: 1px solid var(--color-border-soft); - transition: all 120ms var(--ease-spring); - min-height: 72px; +.usage-hero-metrics { + text-align: center; } -.usage-summary-card-value { - font-size: 26px; +.usage-hero-main { + font-size: 42px; font-weight: 700; color: var(--color-text-primary); - line-height: 1.1; - letter-spacing: -0.02em; + line-height: 1; + letter-spacing: -0.03em; font-variant-numeric: tabular-nums; + animation: usage-hero-count 0.8s ease-out; } -.usage-summary-card-label { - font-size: 11px; - font-weight: 500; - color: var(--color-text-tertiary); - text-transform: uppercase; - letter-spacing: 0.04em; -} - -.usage-copyable { - cursor: pointer; - -webkit-tap-highlight-color: transparent; -} - -.usage-copyable:hover { - border-color: var(--color-brand); - background: var(--color-surface-alt); -} - -.usage-copyable:active { - transform: scale(0.98); -} - -.usage-copyable:focus-visible { - outline: 2px solid var(--color-brand-light); - outline-offset: 2px; -} - -/* ---- Content area ---- */ -.usage-content { - position: relative; -} - -.usage-content-loading { - opacity: 0.7; - pointer-events: none; +@keyframes usage-hero-count { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } } -.usage-content-overlay { - position: absolute; - inset: 0; +.usage-hero-sub { + margin-top: 8px; + font-size: 13px; + color: var(--color-text-tertiary); display: flex; - align-items: flex-start; - justify-content: flex-end; - padding: 12px; - z-index: 1; -} - -.usage-spinner { - width: 16px; - height: 16px; - border-radius: 50%; - border: 2px solid var(--color-border-soft); - border-top-color: var(--color-brand); - animation: usage-spin 0.8s linear infinite; -} - -@keyframes usage-spin { - to { transform: rotate(360deg); } -} - -/* ---- Cards ---- */ -.usage-card { - padding: 18px; - border-radius: var(--radius-md); - background: var(--color-surface); - border: 1px solid var(--color-border-soft); - margin-bottom: 12px; -} - -.usage-card-head { - margin-bottom: 14px; -} - -.usage-card-title { - font-size: 14px; - font-weight: 700; - color: var(--color-text-primary); - margin-bottom: 2px; -} - -.usage-card-subtitle { - font-size: 11px; - color: var(--color-text-muted); -} - -/* ---- Chart grid ---- */ -.usage-chart-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); + align-items: center; + justify-content: center; gap: 12px; + flex-wrap: wrap; } -.usage-card-wide { - grid-column: 1 / -1; +.usage-hero-delta { + font-size: 12px; + font-weight: 600; + padding: 2px 8px; + border-radius: 12px; } -/* ---- Daily chart ---- */ -.usage-daily-chart { - display: flex; - align-items: flex-end; - gap: 4px; - height: 140px; - padding: 0 2px; +.usage-hero-delta.delta-up { + color: var(--color-delta-up); + background: rgba(76, 175, 80, 0.12); } -.usage-daily-bar-group { - flex: 1; - display: flex; - flex-direction: column; - align-items: center; - gap: 6px; - cursor: pointer; - min-width: 0; - -webkit-tap-highlight-color: transparent; +.usage-hero-delta.delta-down { + color: var(--color-delta-down); + background: rgba(244, 67, 54, 0.12); } -.usage-daily-bar-stack { - width: 100%; - max-width: 32px; - height: 100px; - display: flex; - flex-direction: column; - justify-content: flex-end; - position: relative; - border-radius: 4px 4px 0 0; +/* ---- 波浪图 ---- */ +.usage-wave-section { overflow: hidden; - background: var(--color-surface-alt); } -.usage-daily-bar-fill { - width: 100%; - border-radius: 4px 4px 0 0; - background: var(--color-brand); - transition: height 200ms var(--ease-spring); - min-height: 2px; +.usage-wave-container { + margin-top: 12px; + position: relative; } -.usage-daily-bar-prev { +.usage-wave-chart { width: 100%; - background: var(--color-text-muted); - opacity: 0.35; - border-radius: 4px 4px 0 0; + height: 140px; + display: block; } -.usage-daily-bar-group.active .usage-daily-bar-fill { - background: var(--color-brand-dark); - box-shadow: 0 0 8px rgba(200, 121, 99, 0.3); +.usage-wave-area { + transition: d 0.4s var(--ease-spring); } -.usage-daily-bar-group:hover .usage-daily-bar-fill { - filter: brightness(1.1); +.usage-wave-line { + transition: d 0.4s var(--ease-spring); } -.usage-daily-bar-label { - font-size: 10px; - color: var(--color-text-muted); - text-align: center; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - max-width: 100%; +.usage-wave-hover-line { + transition: x1 0.2s, x2 0.2s, y1 0.2s, y2 0.2s; } -.usage-daily-bar-group.active .usage-daily-bar-label { - color: var(--color-brand-dark); - font-weight: 700; +.usage-wave-hover-point { + transition: cx 0.2s, cy 0.2s; } -/* ---- Daily legend ---- */ -.usage-daily-legend { +.usage-wave-labels { display: flex; - gap: 16px; - margin-top: 10px; - font-size: 11px; - color: var(--color-text-muted); -} - -.usage-daily-legend-item { - display: inline-flex; - align-items: center; - gap: 5px; + justify-content: space-between; + margin-top: 8px; + padding: 0 4px; } -.usage-daily-legend-swatch { - width: 10px; - height: 10px; - border-radius: 2px; +.usage-wave-label { + font-size: 10px; + color: var(--color-text-muted); + cursor: pointer; + padding: 2px 6px; + border-radius: 4px; + transition: all 0.15s ease-out; + -webkit-tap-highlight-color: transparent; } -.usage-daily-legend-swatch.current { - background: var(--color-brand); +.usage-wave-label:hover { + color: var(--color-text-secondary); + background: var(--color-surface-alt); } -.usage-daily-legend-swatch.prev { - background: var(--color-text-muted); - opacity: 0.35; +.usage-wave-label.active { + color: var(--color-brand); + font-weight: 600; } -/* ---- Day detail ---- */ +/* ---- 日期详情 ---- */ .usage-daydetail { - margin-top: 12px; - padding: 10px 14px; - border-radius: var(--radius-sm); + margin-top: 16px; + padding: 12px 16px; + border-radius: 10px; background: var(--color-surface-alt); } @@ -364,7 +283,7 @@ } .usage-daydetail-date { - font-weight: 700; + font-weight: 600; font-size: 13px; color: var(--color-text-primary); } @@ -380,82 +299,7 @@ color: var(--color-text-muted); } -/* ---- Lists ---- */ -.usage-list { - display: flex; - flex-direction: column; - gap: 8px; -} - -.usage-list-row { - display: grid; - grid-template-columns: 1fr auto; - align-items: center; - gap: 8px 12px; - padding: 8px 10px; - border-radius: var(--radius-sm); - background: var(--color-surface-alt); - transition: background 120ms var(--ease-spring); -} - -.usage-list-row:hover { - background: var(--color-surface-elevated); -} - -.usage-list-title { - font-size: 12px; - font-weight: 600; - color: var(--color-text-primary); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -.usage-list-path { - font-size: 12px; - color: var(--color-text-secondary); - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - font-family: var(--font-family-mono); - font-size: 11px; -} - -.usage-list-stat { - font-size: 13px; - font-weight: 700; - color: var(--color-text-primary); - font-variant-numeric: tabular-nums; - text-align: right; -} - -.usage-list-meta { - grid-column: 1 / -1; - font-size: 11px; - color: var(--color-text-muted); -} - -.usage-list-value { - font-size: 12px; - color: var(--color-text-secondary); - padding: 8px 0; -} - -.usage-progress { - height: 3px; - border-radius: 2px; - background: var(--color-surface-alt); - overflow: hidden; -} - -.usage-progress-fill { - height: 100%; - border-radius: 2px; - background: var(--color-brand); - transition: width 300ms var(--ease-spring); -} - -/* ---- Hourly heatmap ---- */ +/* ---- 热力图 ---- */ .usage-card-hourly-heatmap { overflow-x: auto; } @@ -506,8 +350,9 @@ .hourly-heatmap-cell { flex: 1; min-width: 0; - aspect-ratio: 1; + height: 6px; border-radius: 3px; + transition: background 0.15s ease-out; } .hourly-heatmap-cell.level-0 { background: var(--color-surface-alt); } @@ -521,7 +366,7 @@ align-items: center; gap: 3px; justify-content: flex-end; - margin-top: 8px; + margin-top: 10px; font-size: 10px; color: var(--color-text-muted); } @@ -529,32 +374,199 @@ .hourly-heatmap-legend .hourly-heatmap-cell { width: 11px; height: 11px; - aspect-ratio: auto; } .hourly-heatmap-legend-label { margin: 0 2px; } -/* ---- Empty state ---- */ -.usage-empty { - padding: 32px 16px; - border-radius: var(--radius-md); +/* ---- 列表样式 ---- */ +.usage-list-compact { + display: flex; + flex-direction: column; + gap: 4px; +} + +.usage-list-compact-item { + display: flex; + align-items: flex-start; + gap: 8px; + padding: 8px 10px; + border-radius: 8px; + cursor: pointer; + transition: all 0.15s ease-out; + -webkit-tap-highlight-color: transparent; +} + +.usage-list-compact-item:hover { + background: var(--color-surface-elevated); +} + +.usage-list-compact-item:active { + transform: scale(0.98); +} + +.usage-list-bullet { + color: var(--color-brand); + font-size: 14px; + line-height: 1.4; + flex-shrink: 0; +} + +.usage-list-compact-content { + flex: 1; + min-width: 0; +} + +.usage-list-title { + font-size: 12px; + font-weight: 500; + color: var(--color-text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.usage-list-meta { + font-size: 11px; + color: var(--color-text-muted); + margin-top: 2px; +} + +.usage-list-value { + font-size: 12px; + color: var(--color-text-secondary); + padding: 8px 0; +} + +/* ---- Path 列表 ---- */ +.usage-paths-section { + grid-column: 1 / -1; +} + +.usage-list-paths { + display: flex; + flex-direction: column; + gap: 6px; +} + +.usage-list-path-row { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 10px; + border-radius: 8px; + transition: background 0.15s ease-out; +} + +.usage-list-path-row:hover { background: var(--color-surface-alt); - border: 1px dashed var(--color-border); +} + +.usage-list-path-rank { + width: 18px; + height: 18px; + display: flex; + align-items: center; + justify-content: center; + font-size: 10px; + font-weight: 600; + color: var(--color-brand); + background: var(--color-surface-alt); + border-radius: 4px; + flex-shrink: 0; +} + +.usage-list-path-content { + flex: 1; + display: flex; + justify-content: space-between; + align-items: center; + gap: 10px; + min-width: 0; +} + +.usage-list-path { + font-size: 12px; color: var(--color-text-secondary); - text-align: center; + font-family: var(--font-family-mono); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.usage-list-path-stat { font-size: 13px; + font-weight: 600; + color: var(--color-text-primary); + font-variant-numeric: tabular-nums; + flex-shrink: 0; } -/* ---- Responsive ---- */ +/* ---- 卡片 ---- */ +.usage-card { + padding: 20px; + border-radius: 14px; + background: var(--color-surface); + border: 1px solid var(--color-border-soft); + margin-bottom: 0; +} + +.usage-card-title { + font-size: 14px; + font-weight: 600; + color: var(--color-text-primary); + margin-bottom: 4px; +} + +/* ---- 图表网格 ---- */ +.usage-chart-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 12px; +} + +/* ---- 内容区域 ---- */ +.usage-content { + position: relative; +} + +.usage-content-loading { + opacity: 0.7; + pointer-events: none; +} + +.usage-content-overlay { + position: absolute; + inset: 0; + display: flex; + align-items: flex-start; + justify-content: flex-end; + padding: 12px; + z-index: 1; +} + +.usage-spinner { + width: 16px; + height: 16px; + border-radius: 50%; + border: 2px solid var(--color-border-soft); + border-top-color: var(--color-brand); + animation: usage-spin 0.8s linear infinite; +} + +@keyframes usage-spin { + to { transform: rotate(360deg); } +} + +/* ---- 响应式 ---- */ @media (max-width: 960px) { .usage-chart-grid { grid-template-columns: 1fr; } - .usage-summary-grid { - grid-template-columns: repeat(2, 1fr); + .usage-hero-main { + font-size: 36px; } } @@ -569,20 +581,25 @@ flex-wrap: wrap; } - .usage-summary-grid { - grid-template-columns: repeat(2, 1fr); + .usage-hero { + padding: 20px; + min-height: 120px; } - .usage-summary-card-value { - font-size: 22px; + .usage-hero-main { + font-size: 32px; } - .usage-daily-chart { + .usage-wave-chart { height: 100px; } - .usage-daily-bar-stack { - max-width: 24px; - height: 70px; + .usage-wave-labels { + font-size: 9px; + } + + .hourly-heatmap-wrapper { + min-width: 100%; + overflow-x: auto; } } From 686ec812efbae7b52d9f9a479be0075d92f60f72 Mon Sep 17 00:00:00 2001 From: ymkiux Date: Sat, 23 May 2026 22:40:53 +0800 Subject: [PATCH 03/24] fix(web-ui): resolve /web-ui/ 404 and duplicate path issues - Server: handle /web-ui/ requests by returning index.html - Server: explicitly serve index.html for /web-ui/index.html requests - Client: normalize URL on mount to fix /web-ui/web-ui/ duplicates - Client: redirect /web-ui/ and /web-ui/index.html to /web-ui Fixes the issue where refreshing in sessions tab caused URL to become /web-ui/web-ui/index.html --- cli.js | 16 +++++++++++++++- web-ui/app.js | 24 ++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/cli.js b/cli.js index 676dc856..6b7f4e5a 100644 --- a/cli.js +++ b/cli.js @@ -11408,7 +11408,7 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser res.end(errorBody, 'utf-8'); } }); - } else if (requestPath === '/web-ui') { + } else if (requestPath === '/web-ui' || requestPath === '/web-ui/') { try { const html = readBundledWebUiHtml(htmlPath); res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); @@ -11417,6 +11417,7 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser writeWebUiAssetError(res, requestPath, error); } } else if (requestPath.startsWith('/web-ui/')) { + // Skip the /web-ui/ directory itself, which is handled above const normalized = path.normalize(requestPath).replace(/^([\\.\\/])+/, ''); const filePath = path.join(__dirname, normalized); if (!isPathInside(filePath, webDir)) { @@ -11425,6 +11426,19 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser return; } const relativePath = path.relative(webDir, filePath).replace(/\\/g, '/'); + + // Check if this is a direct request for index.html + if (relativePath === 'index.html' || relativePath === '') { + try { + const html = readBundledWebUiHtml(htmlPath); + res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' }); + res.end(html); + } catch (error) { + writeWebUiAssetError(res, requestPath, error); + } + return; + } + const dynamicAsset = PUBLIC_WEB_UI_DYNAMIC_ASSETS.get(relativePath); if (dynamicAsset) { try { diff --git a/web-ui/app.js b/web-ui/app.js index f58bfc8b..90dec7d3 100644 --- a/web-ui/app.js +++ b/web-ui/app.js @@ -420,6 +420,30 @@ document.addEventListener('DOMContentLoaded', () => { }, mounted() { + // URL 规范化:修复 /web-ui/ 404 和 /web-ui/web-ui/ 重复路径问题 + try { + const url = new URL(window.location.href); + let shouldReplace = false; + // 修复 /web-ui/web-ui/ 重复路径 + if (url.pathname.includes('/web-ui/web-ui/')) { + url.pathname = url.pathname.replace(/\/web-ui\/web-ui\//g, '/web-ui/'); + shouldReplace = true; + } + // 修复 /web-ui/ (斜尾) → /web-ui + if (url.pathname === '/web-ui/') { + url.pathname = '/web-ui'; + shouldReplace = true; + } + // 修复 /web-ui/index.html → /web-ui + if (url.pathname === '/web-ui/index.html') { + url.pathname = '/web-ui'; + shouldReplace = true; + } + if (shouldReplace) { + window.history.replaceState(null, '', url.toString()); + } + } catch (_) {} + if (typeof this.initI18n === 'function') { this.initI18n(); } From 05035ac7b89e583f6f963f5af0af8c708e0e9ab2 Mon Sep 17 00:00:00 2001 From: ymkiux Date: Sat, 23 May 2026 23:20:41 +0800 Subject: [PATCH 04/24] fix(web-ui): strengthen URL normalization to handle nested duplicates - Use do-while loop to iteratively remove all /web-ui/web-ui/ patterns - Handle edge cases like /web-ui/web-ui/web-ui/index.html - Add additional check for /web-ui/web-ui/ prefix pattern --- web-ui/app.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/web-ui/app.js b/web-ui/app.js index 90dec7d3..fa29fb30 100644 --- a/web-ui/app.js +++ b/web-ui/app.js @@ -424,11 +424,24 @@ document.addEventListener('DOMContentLoaded', () => { try { const url = new URL(window.location.href); let shouldReplace = false; - // 修复 /web-ui/web-ui/ 重复路径 - if (url.pathname.includes('/web-ui/web-ui/')) { + + // 修复多层 /web-ui/ 重复路径(如 /web-ui/web-ui/web-ui/index.html) + // 使用循环确保所有重复都被移除 + let prevPathname; + do { + prevPathname = url.pathname; + // 移除连续的 /web-ui/web-ui/ 为单个 /web-ui/ url.pathname = url.pathname.replace(/\/web-ui\/web-ui\//g, '/web-ui/'); + // 移除开头的 /web-ui/web-ui/(如果有) + if (url.pathname.startsWith('/web-ui/web-ui/')) { + url.pathname = '/web-ui/' + url.pathname.slice('/web-ui/web-ui/'.length); + } + } while (url.pathname !== prevPathname); + + if (prevPathname !== url.pathname) { shouldReplace = true; } + // 修复 /web-ui/ (斜尾) → /web-ui if (url.pathname === '/web-ui/') { url.pathname = '/web-ui'; From 3ded8c388afcf6738b0205f87a91009f3c4f683f Mon Sep 17 00:00:00 2001 From: ymkiux Date: Sun, 24 May 2026 00:12:45 +0800 Subject: [PATCH 05/24] test(web-ui): add URL routing and normalization tests - Add test-web-ui-url-routing.js E2E test for server routing - Add test-web-ui-url-normalization.mjs unit test (24 cases) - Update test-web-ui-assets.js expectations for /web-ui/ fix - Fix shouldReplace check logic in app.js (use originalPathname) --- tests/e2e/test-web-ui-assets.js | 10 +- tests/e2e/test-web-ui-url-routing.js | 209 ++++++++++++++++ tests/unit/test-web-ui-url-normalization.mjs | 249 +++++++++++++++++++ web-ui/app.js | 3 +- 4 files changed, 467 insertions(+), 4 deletions(-) create mode 100644 tests/e2e/test-web-ui-url-routing.js create mode 100644 tests/unit/test-web-ui-url-normalization.mjs diff --git a/tests/e2e/test-web-ui-assets.js b/tests/e2e/test-web-ui-assets.js index 1db5c145..863108f7 100644 --- a/tests/e2e/test-web-ui-assets.js +++ b/tests/e2e/test-web-ui-assets.js @@ -64,10 +64,14 @@ module.exports = async function testWebUiAssets(ctx) { assert(!/ +
+
+ {{ t('market.title') }} +
+ -
- -
- - -
- -
{{ skillsRootPath || skillsDefaultRootPath }}
- -
-
- {{ t('market.summary.target') }} - {{ skillsTargetLabel }} -
-
- {{ t('market.summary.total') }} - {{ skillsList.length }} -
-
- {{ t('market.summary.configured') }} - {{ skillsConfiguredCount }} -
-
- {{ t('market.summary.missing') }} - {{ skillsMissingSkillFileCount }} -
-
- {{ t('market.summary.importable') }} - {{ skillsImportList.length }} -
-
- {{ t('market.summary.importableDirect') }} - {{ skillsImportConfiguredCount }} -
-
-
-
-
-
{{ t('market.installed.title') }}
-
{{ t('market.installed.note') }}
-
- -
-
{{ t('market.local.loading') }}
-
{{ t('market.local.empty') }}
-
-
-
-
{{ skill.displayName || skill.name }}
-
{{ skill.description || skill.path }}
-
- - {{ skill.hasSkillFile ? t('market.pill.verified') : t('market.pill.missingSkill') }} - -
-
+ +
+
+
+ {{ t('market.installed.title') }} + {{ skillsList.length }} +
+
- -
-
-
-
{{ t('market.import.title') }}
-
{{ t('market.import.note', { target: skillsTargetLabel }) }}
-
- -
-
{{ t('market.import.loading') }}
-
{{ t('market.import.empty') }}
-
-
-
-
{{ skill.displayName || skill.name }}
-
{{ skill.sourceLabel }} · {{ skill.sourcePath }}
-
- - {{ skill.hasSkillFile ? t('market.pill.importableDirect') : t('market.pill.importMissing') }} - +
{{ t('market.local.loading') }}
+
{{ t('market.local.empty') }}
+
+
+
+ {{ skill.displayName || skill.name }} + {{ skill.path }}
+ + {{ skill.hasSkillFile ? t('market.pill.verified') : t('market.pill.missingSkill') }} +
+
-
-
-
-
{{ t('market.actions.title') }}
-
{{ t('market.actions.note') }}
-
-
-
- - +
+
{{ t('market.import.loading') }}
+
{{ t('market.import.empty') }}
+
+ +
+ -
-
- -
-
-
-
{{ t('market.help.title') }}
-
-
-
-
-
-
{{ t('market.help.target.title') }}
-
{{ t('market.help.target.copy', { target: skillsTargetLabel }) }}
-
-
-
-
-
{{ t('market.help.crossImport.title') }}
-
{{ t('market.help.crossImport.copy') }}
-
-
-
-
-
{{ t('market.help.zipImport.title') }}
-
{{ t('market.help.zipImport.copy') }}
+ +
+
+
+ {{ skill.displayName || skill.name }} + {{ skill.sourceLabel }}
+
diff --git a/web-ui/styles/skills-market.css b/web-ui/styles/skills-market.css index 3e8dd9d7..9851f469 100644 --- a/web-ui/styles/skills-market.css +++ b/web-ui/styles/skills-market.css @@ -1,3 +1,297 @@ +/* ========================= + Minimalist Skills Flow Layout + ========================= */ + +/* Header with title and target switch */ +.skills-minimal-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--spacing-sm); + margin-bottom: var(--spacing-md); + padding-bottom: var(--spacing-sm); + border-bottom: 1px solid var(--color-border-soft); +} + +.skills-header-left { + display: flex; + flex-direction: column; + gap: 10px; +} + +.skills-header-title { + font-size: 20px; + font-weight: var(--font-weight-primary); + color: var(--color-text-primary); + letter-spacing: -0.01em; +} + +.skills-target-switch { + display: inline-flex; + background: var(--color-surface-alt); + border-radius: var(--radius-full); + padding: 3px; + gap: 3px; +} + +.skills-target-chip { + border: none; + background: transparent; + color: var(--color-text-secondary); + padding: 6px 14px; + font-size: var(--font-size-caption); + font-weight: var(--font-weight-caption); + border-radius: var(--radius-full); + cursor: pointer; + transition: all var(--transition-fast) var(--ease-smooth); +} + +.skills-target-chip:disabled, +.skills-target-chip[disabled] { + cursor: not-allowed; + opacity: 0.5; + pointer-events: none; +} + +.skills-target-chip.active { + background: var(--color-surface); + color: var(--color-text-primary); + box-shadow: 0 1px 3px rgba(0,0,0,0.06); +} + +.skills-target-chip:hover:not(.active):not(:disabled) { + color: var(--color-text-primary); +} + +.skills-header-actions { + display: flex; + gap: 6px; +} + +/* Flow panels */ +.skills-flow-panel { + margin-bottom: var(--spacing-md); + background: var(--color-surface); + border: 1px solid var(--color-border); + border-radius: var(--radius-lg); + overflow: hidden; +} + +.skills-flow-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: var(--spacing-sm) var(--spacing-md); + border-bottom: 1px solid var(--color-border-soft); + background: linear-gradient(to bottom, rgba(255,255,255,0.5) 0%, rgba(255,255,255,0) 100%); +} + +.skills-flow-title-wrap { + display: flex; + align-items: baseline; + gap: 8px; +} + +.skills-flow-title { + font-size: var(--font-size-body); + font-weight: var(--font-weight-secondary); + color: var(--color-text-secondary); +} + +.skills-flow-count { + font-size: var(--font-size-caption); + color: var(--color-text-tertiary); + background: var(--color-surface-alt); + padding: 2px 8px; + border-radius: var(--radius-full); + font-weight: var(--font-weight-caption); +} + +/* Flow list */ +.skills-flow-list { + display: flex; + flex-direction: column; +} + +.skills-flow-item { + display: flex; + justify-content: space-between; + align-items: center; + gap: var(--spacing-sm); + padding: var(--spacing-sm) var(--spacing-md); + border-bottom: 1px solid var(--color-border-soft); + transition: background var(--transition-fast) var(--ease-smooth); + min-height: 56px; +} + +.skills-flow-item:last-child { + border-bottom: none; +} + +.skills-flow-item:hover { + background: rgba(255,255,255,0.4); +} + +.skills-flow-item.has-issue { + background: rgba(255,243,236,0.3); +} + +.skills-flow-item.has-issue:hover { + background: rgba(255,243,236,0.5); +} + +.skills-flow-main { + display: flex; + flex-direction: column; + gap: 3px; + min-width: 0; + flex: 1; +} + +.skills-flow-name { + font-size: var(--font-size-body); + font-weight: var(--font-weight-secondary); + color: var(--color-text-primary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.skills-flow-path, +.skills-flow-meta { + font-size: var(--font-size-caption); + color: var(--color-text-tertiary); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.skills-flow-status { + font-size: var(--font-size-caption); + padding: 4px 10px; + border-radius: var(--radius-full); + font-weight: var(--font-weight-caption); + white-space: nowrap; +} + +.skills-flow-status.success { + background: rgba(52,168,83,0.1); + color: rgba(52,168,83,0.9); +} + +.skills-flow-status.warning { + background: rgba(255,159,10,0.1); + color: rgba(255,159,10,0.9); +} + +.skills-flow-add { + border: none; + background: var(--color-surface-alt); + color: var(--color-text-secondary); + width: 36px; + height: 36px; + border-radius: var(--radius-full); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + transition: all var(--transition-fast) var(--ease-smooth); + flex-shrink: 0; +} + +.skills-flow-add:hover:not(:disabled) { + background: var(--color-brand); + color: white; +} + +.skills-flow-add:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Quick actions in import panel */ +.skills-flow-actions { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: var(--spacing-xs); + padding: var(--spacing-sm) var(--spacing-md); + border-bottom: 1px solid var(--color-border-soft); + background: rgba(255,255,255,0.3); +} + +.skills-action-btn { + border: 1px solid var(--color-border); + background: var(--color-surface); + color: var(--color-text-secondary); + padding: 10px 14px; + border-radius: var(--radius-md); + font-size: var(--font-size-caption); + font-weight: var(--font-weight-caption); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + transition: all var(--transition-fast) var(--ease-smooth); +} + +.skills-action-btn:hover:not(:disabled) { + border-color: var(--color-brand); + background: rgba(255,243,236,0.5); + color: var(--color-brand-dark); +} + +.skills-action-btn:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +/* Loading and empty states */ +.skills-flow-loading, +.skills-flow-empty { + padding: var(--spacing-md); + text-align: center; + color: var(--color-text-tertiary); + font-size: var(--font-size-caption); +} + +/* Spinning icon */ +@keyframes spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +.spin { + animation: spin 0.8s linear infinite; +} + +/* Responsive */ +@media (max-width: 600px) { + .skills-minimal-header { + flex-direction: column; + align-items: flex-start; + gap: var(--spacing-sm); + } + + .skills-header-actions { + align-self: flex-end; + } + + .skills-flow-actions { + grid-template-columns: 1fr; + } + + .skills-flow-item { + flex-wrap: wrap; + } + + .skills-flow-status, + .skills-flow-add { + margin-left: auto; + } +} + +/* Legacy styles for modal (preserved) */ .agent-list { display: flex; flex-direction: column; From 545ac9fc2bd742ad39676341535443a189d911f6 Mon Sep 17 00:00:00 2001 From: ymkiux Date: Sun, 24 May 2026 02:41:21 +0800 Subject: [PATCH 14/24] refactor(web): implement static URL, always stay at / - Remove all URL routing, URL stays as pure '/' - Delete query parameters and hash from browser address bar - Remove history.pushState/replaceState operations for tab switching - Remove session filter URL synchronization - Share links still work (generate URL with params for clipboard) - When opening share link, params are applied then URL cleaned - All state managed via memory + localStorage Changes: - app.js: Clean URL on mount, remove resource path fix logic - app.methods.navigation.mjs: Remove URL operations in switchMainTab - sessions-filters-url.mjs: Empty syncSessionsFilterUrl, fix share URL base - app.methods.session-browser.mjs: Clean URL after applying URL state --- web-ui/app.js | 32 ++++++++----------- web-ui/modules/app.methods.navigation.mjs | 15 +-------- .../modules/app.methods.session-browser.mjs | 7 ++++ web-ui/modules/sessions-filters-url.mjs | 18 ++++------- 4 files changed, 27 insertions(+), 45 deletions(-) diff --git a/web-ui/app.js b/web-ui/app.js index ddf24ed8..48bca3ff 100644 --- a/web-ui/app.js +++ b/web-ui/app.js @@ -420,29 +420,23 @@ document.addEventListener('DOMContentLoaded', () => { }, mounted() { - // URL 规范化:/web-ui 入口重定向到 /,修复资源重复路径 + // URL 规范化:将 /web-ui/* 重定向到根路径 / try { - const url = new URL(window.location.href); - let pathname = url.pathname; - - // 循环修复多层 /web-ui/ 重复 - let prevPathname; - do { - prevPathname = pathname; - pathname = pathname.replace(/\/+web-ui\/+web-ui\/+/g, '/web-ui/'); - } while (pathname !== prevPathname); - - // /web-ui/* 入口重定向到根路径 + const pathname = window.location.pathname; if (pathname === '/web-ui' || pathname === '/web-ui/' || pathname === '/web-ui/index.html') { - const targetUrl = new URL(url); - targetUrl.pathname = '/'; - window.location.replace(targetUrl.toString()); + const url = new URL(window.location.href); + url.pathname = '/'; + // 移除查询参数和 hash,保持 URL 纯净 + url.search = ''; + url.hash = ''; + window.location.replace(url.toString()); return; } - - // 修复资源路径中的重复 /web-ui/web-ui/ - if (pathname !== url.pathname) { - url.pathname = pathname; + // 清理任何查询参数和 hash,保持 URL 为 / + if (window.location.search || window.location.hash) { + const url = new URL(window.location.href); + url.search = ''; + url.hash = ''; window.history.replaceState(null, '', url.toString()); } } catch (_) {} diff --git a/web-ui/modules/app.methods.navigation.mjs b/web-ui/modules/app.methods.navigation.mjs index 03220d95..9491f2d6 100644 --- a/web-ui/modules/app.methods.navigation.mjs +++ b/web-ui/modules/app.methods.navigation.mjs @@ -419,20 +419,7 @@ mainTab: targetTab, configMode: targetTab === 'config' ? this.configMode : this.configMode }); - if (targetTab !== 'sessions') { - try { - const url = new URL(window.location.href); - if (url.pathname !== '/session') { - url.searchParams.delete('s_source'); - url.searchParams.delete('s_path'); - url.searchParams.delete('s_query'); - url.searchParams.delete('s_role'); - url.searchParams.delete('s_time'); - url.searchParams.delete('tab'); - window.history.replaceState(null, '', url.toString()); - } - } catch (_) {} - } + // URL 保持静态,不写入任何状态 this.cancelTouchNavIntentReset(); if (targetTab === 'sessions') { this.cancelScheduledSessionTabDeferredTeardown(); diff --git a/web-ui/modules/app.methods.session-browser.mjs b/web-ui/modules/app.methods.session-browser.mjs index 12dcae04..c87bdafa 100644 --- a/web-ui/modules/app.methods.session-browser.mjs +++ b/web-ui/modules/app.methods.session-browser.mjs @@ -264,6 +264,13 @@ export function createSessionBrowserMethods(options = {}) { }); if (urlState) { applySessionsFilterUrlState(this, urlState); + // 清理 URL,保持静态 + try { + const url = new URL(window.location.href); + url.search = ''; + url.hash = ''; + window.history.replaceState(null, '', url.toString()); + } catch (_) {} try { const sortCache = localStorage.getItem('codexmateSessionSortMode'); this.sessionSortMode = normalizeSortMode(sortCache); diff --git a/web-ui/modules/sessions-filters-url.mjs b/web-ui/modules/sessions-filters-url.mjs index 6fb1bbaa..b2bf8890 100644 --- a/web-ui/modules/sessions-filters-url.mjs +++ b/web-ui/modules/sessions-filters-url.mjs @@ -57,18 +57,15 @@ export function applySessionsFilterUrlState(vm, state) { export function buildSessionsFilterShareUrl(vm) { try { - const url = new URL(window.location.href); - if (url.pathname === '/session') return ''; + // 使用干净的根路径作为基础 URL + const baseUrl = window.location.origin + '/'; + const url = new URL(baseUrl); + url.searchParams.set('tab', 'sessions'); url.searchParams.set('s_source', String(vm.sessionFilterSource || 'all')); if (vm.sessionPathFilter) url.searchParams.set('s_path', String(vm.sessionPathFilter || '')); - else url.searchParams.delete('s_path'); if (vm.sessionQuery && isSessionQueryEnabled(vm.sessionFilterSource)) url.searchParams.set('s_query', String(vm.sessionQuery || '')); - else url.searchParams.delete('s_query'); if (vm.sessionRoleFilter && vm.sessionRoleFilter !== 'all') url.searchParams.set('s_role', String(vm.sessionRoleFilter || 'all')); - else url.searchParams.delete('s_role'); if (vm.sessionTimePreset && vm.sessionTimePreset !== 'all') url.searchParams.set('s_time', String(vm.sessionTimePreset || 'all')); - else url.searchParams.delete('s_time'); - url.searchParams.set('tab', 'sessions'); return url.toString(); } catch (_) { return ''; @@ -76,10 +73,7 @@ export function buildSessionsFilterShareUrl(vm) { } export function syncSessionsFilterUrl(vm) { - const url = buildSessionsFilterShareUrl(vm); - if (!url) return; - try { - window.history.replaceState(null, '', url); - } catch (_) {} + // URL 保持静态,不同步状态到 URL + // 所有状态通过 localStorage 管理 } From 260c6135867867e12f9a0f7e8d2860cb056e7c50 Mon Sep 17 00:00:00 2001 From: ymkiux Date: Sun, 24 May 2026 02:53:38 +0800 Subject: [PATCH 15/24] chore: bump version to 0.0.35 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 59dbe430..d413becb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "codexmate", - "version": "0.1.0", + "version": "0.0.35", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codexmate", - "version": "0.1.0", + "version": "0.0.35", "license": "Apache-2.0", "dependencies": { "@iarna/toml": "^2.2.5", diff --git a/package.json b/package.json index 722a9e2b..28716659 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codexmate", - "version": "0.0.34", + "version": "0.0.35", "description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具", "main": "cli.js", "bin": { From 324c62ea6daf9f3911a9d9e9865c4ad7547eb79a Mon Sep 17 00:00:00 2001 From: ymkiux Date: Sun, 24 May 2026 02:59:27 +0800 Subject: [PATCH 16/24] fix(ci): restore missing CI scripts in package.json --- package.json | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 28716659..d92243d5 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,18 @@ "release:npm": "node tools/release/publish-npm.js", "docs:dev": "node ./node_modules/vitepress/dist/node/cli.js dev site", "docs:build": "node ./node_modules/vitepress/dist/node/cli.js build site", - "docs:preview": "node ./node_modules/vitepress/dist/node/cli.js preview site" + "docs:preview": "node ./node_modules/vitepress/dist/node/cli.js preview site", + "ci:install": "node tools/ci/run-check.js install", + "ci:lint": "node tools/ci/run-check.js lint", + "ci:test": "node tools/ci/run-check.js test", + "lint": "node tools/dev/lint.js", + "test": "npm run test:unit && npm run test:e2e", + "test:ci": "node tools/ci/run-check.js all", + "test:unit": "node tests/unit/run.mjs", + "test:e2e": "node tests/e2e/run.js", + "setup:git": "git remote set-url origin https://github.com/SakuraByteCore/codexmate.git && gh auth setup-git", + "reset:dev": "node tools/dev/reset-and-dev.js", + "pretest": "node tools/ci/ensure-test-deps.js" }, "dependencies": { "@iarna/toml": "^2.2.5", From 060f5e981da8f20e6c53f097d1122d7765e385f4 Mon Sep 17 00:00:00 2001 From: ymkiux Date: Sun, 24 May 2026 09:57:31 +0800 Subject: [PATCH 17/24] test: update unit tests to match new web-ui structure - Fix sessionUsageDaily -> sessionUsageHourlyHeatmap assertion - Remove obsolete loadSkillsMarketOverview and market-action-grid checks - Update skills-target-chip selector (was market-target-chip) - Fix provider card disabled state and tabindex assertions - Remove market.pill importableDirect/importMissing checks --- tests/unit/config-tabs-ui.test.mjs | 47 +++++++++------------- tests/unit/web-run-host.test.mjs | 16 +++----- tests/unit/web-ui-behavior-parity.test.mjs | 5 ++- 3 files changed, 29 insertions(+), 39 deletions(-) diff --git a/tests/unit/config-tabs-ui.test.mjs b/tests/unit/config-tabs-ui.test.mjs index fef2a14d..85039392 100644 --- a/tests/unit/config-tabs-ui.test.mjs +++ b/tests/unit/config-tabs-ui.test.mjs @@ -131,11 +131,13 @@ test('config template keeps expected config tabs in top and side navigation', () assert.match(html, /:aria-selected="mainTab === 'usage'"/); assert.match(html, /id="panel-usage"/); assert.match(html, /v-show="mainTab === 'usage'"/); - assert.match(usagePanel, /sessionsUsageLoading && !sessionsUsageList\.length" class="session-empty">\{\{\s*t\('usage\.loading'\)\s*\}\}/); - assert.match(usagePanel, /v-else-if="!sessionsUsageList\.length" class="usage-empty">\{\{\s*t\('usage\.empty'\)\s*\}\}/); + assert.match(usagePanel, /sessionsUsageLoading && !sessionsUsageList\.length" class="usage-empty-state">/); + assert.match(usagePanel, /class="usage-empty-text">\{\{\s*t\('usage\.loading'\)\s*\}\}<\/p>/); + assert.match(usagePanel, /sessionsUsageError && !sessionsUsageList\.length" class="usage-empty-state">/); + assert.match(usagePanel, /v-else-if="!sessionsUsageList\.length" class="usage-empty-state">/); + assert.match(usagePanel, /class="usage-empty-text">\{\{\s*t\('usage\.empty'\)\s*\}\}<\/p>/); assert.match(usagePanel, /sessionUsageCharts\.topPaths/); - assert.match(usagePanel, /sessionUsageDaily/); + assert.match(usagePanel, /sessionUsageHourlyHeatmap/); assert.match(html, /data-main-tab="market"/); assert.match(html, /onMainTabPointerDown\('market', \$event\)/); assert.match(html, /onMainTabClick\('market', \$event\)/); @@ -155,31 +157,21 @@ test('config template keeps expected config tabs in top and side navigation', () assert.match(html, /installTroubleshootingTips/); assert.doesNotMatch(html, /Skills<\/span>/); assert.doesNotMatch(html, /openSkillsManager\(\{ targetApp: 'codex' \}\)/); - assert.match(html, /loadSkillsMarketOverview\(\{ forceRefresh: true, silent: false \}\)/); - assert.match(html, /class="market-grid"/); - assert.match(html, /class="market-action-grid"/); + assert.match(html, /class="skills-flow-panel"/); assert.match(html, /skillsTargetApp === 'codex'/); assert.match(html, /skillsTargetApp === 'claude'/); assert.match(html, /setSkillsTargetApp\('codex', \{ silent: false \}\)/); assert.match(html, /setSkillsTargetApp\('claude', \{ silent: false \}\)/); const targetSwitchButtons = [...html.matchAll( - //g + //g )]; - assert.strictEqual(targetSwitchButtons.length, 4); + assert.strictEqual(targetSwitchButtons.length, 2); for (const [buttonMarkup] of targetSwitchButtons) { assert.match(buttonMarkup, /:disabled="loading \|\| !!initError \|\| skillsMarketBusy"/); } - assert.match(html, / - - - - - - - -