codexmate 0.0.34 → 0.0.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/README.md +14 -5
  2. package/README.zh.md +14 -5
  3. package/cli.js +74 -61
  4. package/package.json +2 -1
  5. package/web-ui/app.js +32 -2
  6. package/web-ui/index.html +1 -1
  7. package/web-ui/logic.sessions.mjs +6 -5
  8. package/web-ui/modules/app.computed.dashboard.mjs +4 -0
  9. package/web-ui/modules/app.computed.session.mjs +147 -6
  10. package/web-ui/modules/app.methods.claude-config.mjs +4 -0
  11. package/web-ui/modules/app.methods.navigation.mjs +32 -16
  12. package/web-ui/modules/app.methods.session-browser.mjs +7 -0
  13. package/web-ui/modules/app.methods.session-trash.mjs +30 -0
  14. package/web-ui/modules/i18n.dict.mjs +5 -0
  15. package/web-ui/modules/sessions-filters-url.mjs +65 -12
  16. package/web-ui/modules/skills.methods.mjs +31 -0
  17. package/web-ui/partials/index/layout-header.html +20 -11
  18. package/web-ui/partials/index/panel-config-claude.html +5 -3
  19. package/web-ui/partials/index/panel-config-codex.html +1 -1
  20. package/web-ui/partials/index/panel-market.html +76 -149
  21. package/web-ui/partials/index/panel-sessions.html +2 -2
  22. package/web-ui/partials/index/panel-settings.html +4 -2
  23. package/web-ui/partials/index/panel-usage.html +111 -68
  24. package/web-ui/res/vue.runtime.global.prod.js +7 -0
  25. package/web-ui/res/web-ui-render.precompiled.js +7269 -0
  26. package/web-ui/session-helpers.mjs +15 -4
  27. package/web-ui/source-bundle.cjs +73 -1
  28. package/web-ui/styles/base-theme.css +10 -0
  29. package/web-ui/styles/layout-shell.css +65 -27
  30. package/web-ui/styles/navigation-panels.css +8 -0
  31. package/web-ui/styles/responsive.css +50 -9
  32. package/web-ui/styles/sessions-usage.css +501 -336
  33. package/web-ui/styles/skills-market.css +294 -0
  34. package/web-ui/styles/titles-cards.css +14 -0
@@ -4,172 +4,99 @@
4
4
  id="panel-market"
5
5
  role="tabpanel"
6
6
  aria-labelledby="tab-market">
7
- <div class="selector-section market-overview-section">
8
- <div class="selector-header market-overview-header">
9
- <div>
10
- <span class="selector-title">{{ t('market.title') }}</span>
11
- <div class="skills-panel-note">{{ t('market.subtitle') }}</div>
12
- </div>
13
- <div class="settings-tab-actions market-header-actions">
14
- <button type="button" class="btn-tool btn-tool-compact" @click="loadSkillsMarketOverview({ forceRefresh: true, silent: false })" :disabled="loading || !!initError || skillsMarketBusy">
15
- {{ skillsMarketLoading ? t('market.refreshing') : t('market.refresh') }}
16
- </button>
17
- <button type="button" class="btn-tool btn-tool-compact" @click="openSkillsManager" :disabled="loading || !!initError || skillsMarketBusy">
18
- {{ t('market.openManager') }}
7
+ <!-- Minimalist header with target switch -->
8
+ <div class="skills-minimal-header">
9
+ <div class="skills-header-left">
10
+ <span class="skills-header-title">{{ t('market.title') }}</span>
11
+ <div class="skills-target-switch" role="group" :aria-label="t('market.target.aria')">
12
+ <button
13
+ type="button"
14
+ :class="['skills-target-chip', { active: skillsTargetApp === 'codex' }]"
15
+ :aria-pressed="skillsTargetApp === 'codex'"
16
+ :disabled="loading || !!initError || skillsMarketBusy"
17
+ @click="setSkillsTargetApp('codex', { silent: false })">
18
+ Codex
19
19
  </button>
20
- <button type="button" class="btn-tool btn-tool-compact" @click="switchMainTab('dashboard')" :disabled="loading || !!initError || skillsMarketBusy">
21
- {{ t('dashboard.doctor.title') }}
20
+ <button
21
+ type="button"
22
+ :class="['skills-target-chip', { active: skillsTargetApp === 'claude' }]"
23
+ :aria-pressed="skillsTargetApp === 'claude'"
24
+ :disabled="loading || !!initError || skillsMarketBusy"
25
+ @click="setSkillsTargetApp('claude', { silent: false })">
26
+ Claude Code
22
27
  </button>
23
28
  </div>
24
29
  </div>
25
-
26
- <div class="market-target-switch" role="group" :aria-label="t('market.target.aria')">
27
- <button
28
- type="button"
29
- :class="['market-target-chip', { active: skillsTargetApp === 'codex' }]"
30
- :aria-pressed="skillsTargetApp === 'codex'"
31
- :disabled="loading || !!initError || skillsMarketBusy"
32
- @click="setSkillsTargetApp('codex', { silent: false })">
33
- Codex
30
+ <div class="skills-header-actions">
31
+ <button type="button" class="btn-icon" @click="openSkillsMenu" :aria-label="t('common.menu')" :title="t('common.menu')">
32
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg>
34
33
  </button>
35
- <button
36
- type="button"
37
- :class="['market-target-chip', { active: skillsTargetApp === 'claude' }]"
38
- :aria-pressed="skillsTargetApp === 'claude'"
39
- :disabled="loading || !!initError || skillsMarketBusy"
40
- @click="setSkillsTargetApp('claude', { silent: false })">
41
- Claude Code
42
- </button>
43
- </div>
44
-
45
- <div class="skills-root-box market-root-box">{{ skillsRootPath || skillsDefaultRootPath }}</div>
46
-
47
- <div class="skills-summary-strip market-summary-strip">
48
- <div class="skills-summary-item">
49
- <span class="skills-summary-label">{{ t('market.summary.target') }}</span>
50
- <strong class="skills-summary-value">{{ skillsTargetLabel }}</strong>
51
- </div>
52
- <div class="skills-summary-item">
53
- <span class="skills-summary-label">{{ t('market.summary.total') }}</span>
54
- <strong class="skills-summary-value">{{ skillsList.length }}</strong>
55
- </div>
56
- <div class="skills-summary-item">
57
- <span class="skills-summary-label">{{ t('market.summary.configured') }}</span>
58
- <strong class="skills-summary-value">{{ skillsConfiguredCount }}</strong>
59
- </div>
60
- <div class="skills-summary-item">
61
- <span class="skills-summary-label">{{ t('market.summary.missing') }}</span>
62
- <strong class="skills-summary-value">{{ skillsMissingSkillFileCount }}</strong>
63
- </div>
64
- <div class="skills-summary-item">
65
- <span class="skills-summary-label">{{ t('market.summary.importable') }}</span>
66
- <strong class="skills-summary-value">{{ skillsImportList.length }}</strong>
67
- </div>
68
- <div class="skills-summary-item">
69
- <span class="skills-summary-label">{{ t('market.summary.importableDirect') }}</span>
70
- <strong class="skills-summary-value">{{ skillsImportConfiguredCount }}</strong>
71
- </div>
72
34
  </div>
73
35
  </div>
74
36
 
75
- <div class="market-grid">
76
- <div class="skills-panel market-panel">
77
- <div class="skills-panel-header">
78
- <div class="skills-panel-title-wrap">
79
- <div class="skills-panel-title">{{ t('market.installed.title') }}</div>
80
- <div class="skills-panel-note">{{ t('market.installed.note') }}</div>
81
- </div>
82
- <button type="button" class="btn-mini" @click="refreshSkillsList({ silent: false })" :disabled="loading || !!initError || skillsMarketBusy">
83
- {{ skillsLoading ? t('market.local.refreshing') : t('market.local.refresh') }}
84
- </button>
85
- </div>
86
- <div v-if="skillsLoading && !skillsMarketLocalLoadedOnce" class="skills-empty-state">{{ t('market.local.loading') }}</div>
87
- <div v-else-if="skillsList.length === 0" class="skills-empty-state">{{ t('market.local.empty') }}</div>
88
- <div v-else class="market-preview-list">
89
- <div v-for="skill in skillsMarketInstalledPreview" :key="'market-local-' + skill.name" class="market-preview-item">
90
- <div class="market-preview-main">
91
- <div class="market-preview-title">{{ skill.displayName || skill.name }}</div>
92
- <div class="market-preview-meta">{{ skill.description || skill.path }}</div>
93
- </div>
94
- <span :class="['pill', skill.hasSkillFile ? 'configured' : 'empty']">
95
- {{ skill.hasSkillFile ? t('market.pill.verified') : t('market.pill.missingSkill') }}
96
- </span>
97
- </div>
98
- </div>
37
+ <!-- Installed skills panel -->
38
+ <div class="skills-flow-panel">
39
+ <div class="skills-flow-header">
40
+ <div class="skills-flow-title-wrap">
41
+ <span class="skills-flow-title">{{ t('market.installed.title') }}</span>
42
+ <span class="skills-flow-count">{{ skillsList.length }}</span>
43
+ </div>
44
+ <button type="button" class="btn-mini" @click="refreshSkillsList({ silent: false })" :disabled="loading || !!initError || skillsMarketBusy">
45
+ <svg v-if="!skillsLoading" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" width="14" height="14"><path d="M17 1l4 4-4 4"/><path d="M3 11V9a4 4 0 014-4h14"/><path d="M3 19l-4-4 4-4"/><path d="M17 9v2a4 4 0 01-4 4H3"/></svg>
46
+ <svg v-else viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" width="14" height="14" class="spin"><path d="M10 2v4m0 8v4M2 10h4m8 0h4m-3.5-6.5l2.5 2.5M4 4l2.5 2.5M16 16l-2.5-2.5M4 16l2.5-2.5"/></svg>
47
+ </button>
99
48
  </div>
100
-
101
- <div class="skills-panel market-panel">
102
- <div class="skills-panel-header">
103
- <div class="skills-panel-title-wrap">
104
- <div class="skills-panel-title">{{ t('market.import.title') }}</div>
105
- <div class="skills-panel-note">{{ t('market.import.note', { target: skillsTargetLabel }) }}</div>
106
- </div>
107
- <button type="button" class="btn-mini" @click="scanImportableSkills({ silent: false })" :disabled="loading || !!initError || skillsMarketBusy">
108
- {{ skillsScanningImports ? t('market.import.scanning') : t('market.import.scan') }}
109
- </button>
110
- </div>
111
- <div v-if="skillsScanningImports && !skillsMarketImportLoadedOnce" class="skills-empty-state">{{ t('market.import.loading') }}</div>
112
- <div v-else-if="skillsImportList.length === 0" class="skills-empty-state">{{ t('market.import.empty') }}</div>
113
- <div v-else class="market-preview-list">
114
- <div v-for="skill in skillsMarketImportPreview" :key="'market-import-' + buildSkillImportKey(skill)" class="market-preview-item">
115
- <div class="market-preview-main">
116
- <div class="market-preview-title">{{ skill.displayName || skill.name }}</div>
117
- <div class="market-preview-meta">{{ skill.sourceLabel }} · {{ skill.sourcePath }}</div>
118
- </div>
119
- <span :class="['pill', skill.hasSkillFile ? 'configured' : 'empty']">
120
- {{ skill.hasSkillFile ? t('market.pill.importableDirect') : t('market.pill.importMissing') }}
121
- </span>
49
+ <div v-if="skillsLoading && !skillsMarketLocalLoadedOnce" class="skills-flow-loading">{{ t('market.local.loading') }}</div>
50
+ <div v-else-if="skillsList.length === 0" class="skills-flow-empty">{{ t('market.local.empty') }}</div>
51
+ <div v-else class="skills-flow-list">
52
+ <div v-for="skill in skillsList" :key="'skill-' + skill.name" class="skills-flow-item" :class="{ 'has-issue': !skill.hasSkillFile }">
53
+ <div class="skills-flow-main">
54
+ <span class="skills-flow-name">{{ skill.displayName || skill.name }}</span>
55
+ <span class="skills-flow-path">{{ skill.path }}</span>
122
56
  </div>
57
+ <span :class="['skills-flow-status', skill.hasSkillFile ? 'success' : 'warning']">
58
+ {{ skill.hasSkillFile ? t('market.pill.verified') : t('market.pill.missingSkill') }}
59
+ </span>
123
60
  </div>
124
61
  </div>
62
+ </div>
125
63
 
126
- <div class="skills-panel market-panel market-actions-panel">
127
- <div class="skills-panel-header">
128
- <div class="skills-panel-title-wrap">
129
- <div class="skills-panel-title">{{ t('market.actions.title') }}</div>
130
- <div class="skills-panel-note">{{ t('market.actions.note') }}</div>
131
- </div>
132
- </div>
133
- <div class="market-action-grid">
134
- <button type="button" class="market-action-card" @click="openSkillsManager" :disabled="loading || !!initError || skillsMarketBusy">
135
- <span class="market-action-title"><svg style="width:14px;height:14px;vertical-align:-2px;margin-right:4px" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="14" height="14" rx="3"/><path d="M7 7h6M7 10h4"/></svg>{{ t('market.action.manage.title') }}</span>
136
- <span class="market-action-copy">{{ t('market.action.manage.copy', { target: skillsTargetLabel }) }}</span>
137
- </button>
138
- <button type="button" class="market-action-card" @click="scanImportableSkills({ silent: false })" :disabled="loading || !!initError || skillsMarketBusy">
139
- <span class="market-action-title"><svg style="width:14px;height:14px;vertical-align:-2px;margin-right:4px" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 12l3-3 3 3"/><path d="M7 3v6"/><path d="M13 7l3 3 3-3"/><path d="M16 16V7"/></svg>{{ t('market.action.crossImport.title') }}</span>
140
- <span class="market-action-copy">{{ t('market.action.crossImport.copy', { target: skillsTargetLabel }) }}</span>
64
+ <!-- Importable skills panel -->
65
+ <div class="skills-flow-panel">
66
+ <div class="skills-flow-header">
67
+ <div class="skills-flow-title-wrap">
68
+ <span class="skills-flow-title">{{ t('market.import.title') }}</span>
69
+ <span class="skills-flow-count">{{ skillsImportList.length }}</span>
70
+ </div>
71
+ <button type="button" class="btn-mini" @click="scanImportableSkills({ silent: false })" :disabled="loading || !!initError || skillsMarketBusy">
72
+ <svg v-if="!skillsScanningImports" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" width="14" height="14"><path d="M17 1l4 4-4 4"/><path d="M3 11V9a4 4 0 014-4h14"/><path d="M3 19l-4-4 4-4"/><path d="M17 9v2a4 4 0 01-4 4H3"/></svg>
73
+ <svg v-else viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" width="14" height="14" class="spin"><path d="M10 2v4m0 8v4M2 10h4m8 0h4m-3.5-6.5l2.5 2.5M4 4l2.5 2.5M16 16l-2.5-2.5M4 16l2.5-2.5"/></svg>
74
+ </button>
75
+ </div>
76
+ <div v-if="skillsScanningImports && !skillsMarketImportLoadedOnce" class="skills-flow-loading">{{ t('market.import.loading') }}</div>
77
+ <div v-else-if="skillsImportList.length === 0" class="skills-flow-empty">{{ t('market.import.empty') }}</div>
78
+ <div v-else>
79
+ <!-- Quick actions -->
80
+ <div class="skills-flow-actions">
81
+ <button type="button" class="skills-action-btn" @click="openSkillsManager" :disabled="loading || !!initError || skillsMarketBusy">
82
+ <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" width="16" height="16"><rect x="3" y="3" width="14" height="14" rx="3"/><path d="M7 7h6M7 10h4"/></svg>
83
+ {{ t('market.action.manage.title') }}
141
84
  </button>
142
- <button type="button" class="market-action-card" @click="triggerSkillsZipImport" :disabled="loading || !!initError || skillsMarketBusy">
143
- <span class="market-action-title"><svg style="width:14px;height:14px;vertical-align:-2px;margin-right:4px" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 14l3-3 3 3"/><path d="M7 4v7"/><rect x="12" y="4" width="5" height="12" rx="1"/></svg>{{ t('market.action.zipImport.title') }}</span>
144
- <span class="market-action-copy">{{ t('market.action.zipImport.copy') }}</span>
85
+ <button type="button" class="skills-action-btn" @click="triggerSkillsZipImport" :disabled="loading || !!initError || skillsMarketBusy">
86
+ <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" width="16" height="16"><path d="M4 14l3-3 3 3"/><path d="M7 4v7"/><rect x="12" y="4" width="5" height="12" rx="1"/></svg>
87
+ {{ t('market.action.zipImport.title') }}
145
88
  </button>
146
89
  </div>
147
- </div>
148
-
149
- <div class="skills-panel market-panel market-panel-wide">
150
- <div class="skills-panel-header">
151
- <div class="skills-panel-title-wrap">
152
- <div class="skills-panel-title">{{ t('market.help.title') }}</div>
153
- </div>
154
- </div>
155
- <div class="market-preview-list">
156
- <div class="market-preview-item">
157
- <div class="market-preview-main">
158
- <div class="market-preview-title">{{ t('market.help.target.title') }}</div>
159
- <div class="market-preview-meta">{{ t('market.help.target.copy', { target: skillsTargetLabel }) }}</div>
160
- </div>
161
- </div>
162
- <div class="market-preview-item">
163
- <div class="market-preview-main">
164
- <div class="market-preview-title">{{ t('market.help.crossImport.title') }}</div>
165
- <div class="market-preview-meta">{{ t('market.help.crossImport.copy') }}</div>
166
- </div>
167
- </div>
168
- <div class="market-preview-item">
169
- <div class="market-preview-main">
170
- <div class="market-preview-title">{{ t('market.help.zipImport.title') }}</div>
171
- <div class="market-preview-meta">{{ t('market.help.zipImport.copy') }}</div>
90
+ <!-- Import list -->
91
+ <div class="skills-flow-list">
92
+ <div v-for="skill in skillsImportList" :key="'import-' + buildSkillImportKey(skill)" class="skills-flow-item">
93
+ <div class="skills-flow-main">
94
+ <span class="skills-flow-name">{{ skill.displayName || skill.name }}</span>
95
+ <span class="skills-flow-meta">{{ skill.sourceLabel }}</span>
172
96
  </div>
97
+ <button type="button" class="skills-flow-add" @click="importSingleSkill(skill)" :disabled="loading || !!initError || skillsMarketBusy">
98
+ <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" width="16" height="16"><path d="M10 4v6m3-3l-3 3-3-3"/></svg>
99
+ </button>
173
100
  </div>
174
101
  </div>
175
102
  </div>
@@ -193,7 +193,7 @@
193
193
  </div>
194
194
  <div class="session-item-meta">
195
195
  <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>
197
197
  <span v-if="getSessionHotLabel(session)" class="session-item-hot">{{ getSessionHotLabel(session) }}</span>
198
198
  <span v-if="session.cwd" class="session-item-cwd session-item-sub">{{ session.cwd }}</span>
199
199
  <div v-if="session.match && session.match.snippets && session.match.snippets.length" class="session-match-snippets">
@@ -212,7 +212,7 @@
212
212
  <div class="session-preview-title">{{ activeSession.title || activeSession.sessionId }}</div>
213
213
  <div class="session-preview-meta">
214
214
  <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>
216
216
  </div>
217
217
  <div class="session-preview-meta" v-if="activeSession.cwd">
218
218
  <span class="session-preview-meta-item">{{ activeSession.cwd }}</span>
@@ -19,7 +19,8 @@
19
19
  :aria-selected="settingsTab === 'general'"
20
20
  :tabindex="settingsTab === 'general' ? 0 : -1"
21
21
  :class="['segmented-option', { active: settingsTab === 'general' }]"
22
- @click="onSettingsTabClick('general')">{{ t('settings.tab.general') }}</button>
22
+ @click="onSettingsTabClick('general')"
23
+ @keydown="onSettingsTabKeydown($event, 'general')">{{ t('settings.tab.general') }}</button>
23
24
  <button
24
25
  id="settings-tab-data"
25
26
  type="button"
@@ -28,7 +29,8 @@
28
29
  :aria-selected="settingsTab === 'data'"
29
30
  :tabindex="settingsTab === 'data' ? 0 : -1"
30
31
  :class="['segmented-option', { active: settingsTab === 'data' }]"
31
- @click="onSettingsTabClick('data')">{{ t('settings.tab.data') }}</button>
32
+ @click="onSettingsTabClick('data')"
33
+ @keydown="onSettingsTabKeydown($event, 'data')">{{ t('settings.tab.data') }}</button>
32
34
  </div>
33
35
  </div>
34
36
 
@@ -1,4 +1,4 @@
1
- <!-- Usage 统计 -->
1
+ <!-- Usage 统计 - 流光设计 -->
2
2
  <div
3
3
  v-show="mainTab === 'usage'"
4
4
  class="mode-content"
@@ -11,84 +11,118 @@
11
11
  <button type="button" class="usage-range-btn" :class="{ active: sessionsUsageTimeRange === '7d' }" @click="setSessionsUsageTimeRange('7d')">{{ t('usage.range.7d') }}</button>
12
12
  <button type="button" class="usage-range-btn" :class="{ active: sessionsUsageTimeRange === '30d' }" @click="setSessionsUsageTimeRange('30d')">{{ t('usage.range.30d') }}</button>
13
13
  <button type="button" class="usage-range-btn" :class="{ active: sessionsUsageTimeRange === 'all' }" @click="setSessionsUsageTimeRange('all')">{{ t('usage.range.all') }}</button>
14
- <button type="button" class="usage-range-btn usage-range-btn-compare" :class="{ active: sessionsUsageCompareEnabled }" @click="toggleSessionsUsageCompare" :disabled="sessionsUsageTimeRange === 'all'">{{ t('usage.compare.toggle') }}</button>
15
14
  <button type="button" class="usage-range-btn usage-range-btn-icon" @click="loadSessionsUsage({ forceRefresh: true, range: sessionsUsageTimeRange })" :disabled="sessionsUsageLoading" :title="t('usage.refresh')">
16
15
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" class="btn-icon-sm"><path d="M21 2v6h-6M3 12a9 9 0 0 1 15-6.7L21 8M3 22v-6h6M21 12a9 9 0 0 1-15 6.7L3 16"/></svg>
17
16
  </button>
18
17
  </div>
19
18
  </div>
20
19
 
21
- <div v-if="sessionsUsageLoading && !sessionsUsageList.length" class="session-empty">{{ t('usage.loading') }}</div>
22
- <div v-else-if="sessionsUsageError && !sessionsUsageList.length" class="usage-empty">{{ sessionsUsageError }}</div>
23
- <div v-else-if="!sessionsUsageList.length" class="usage-empty">{{ t('usage.empty') }}</div>
20
+ <div v-if="sessionsUsageLoading && !sessionsUsageList.length" class="usage-empty-state">
21
+ <div class="usage-empty-illustration">
22
+ <svg viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
23
+ <circle cx="32" cy="32" r="28" stroke="currentColor" stroke-width="2" stroke-dasharray="4 4" opacity="0.3"/>
24
+ <circle cx="32" cy="32" r="20" stroke="currentColor" stroke-width="2" opacity="0.5"/>
25
+ <path d="M32 20V32L40 36" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
26
+ </svg>
27
+ </div>
28
+ <p class="usage-empty-text">{{ t('usage.loading') }}</p>
29
+ </div>
30
+ <div v-else-if="sessionsUsageError && !sessionsUsageList.length" class="usage-empty-state">
31
+ <div class="usage-empty-illustration">
32
+ <svg viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
33
+ <path d="M20 20L44 44M44 20L20 44" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
34
+ </svg>
35
+ </div>
36
+ <p class="usage-empty-text">{{ sessionsUsageError }}</p>
37
+ </div>
38
+ <div v-else-if="!sessionsUsageList.length" class="usage-empty-state">
39
+ <div class="usage-empty-illustration">
40
+ <svg viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
41
+ <rect x="12" y="16" width="40" height="32" rx="2" stroke="currentColor" stroke-width="2"/>
42
+ <path d="M20 26H44M20 32H36M20 38H32" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
43
+ </svg>
44
+ </div>
45
+ <p class="usage-empty-text">{{ t('usage.empty') }}</p>
46
+ </div>
24
47
  <template v-else>
25
48
  <div class="usage-content" :class="{ 'usage-content-loading': sessionsUsageLoading }">
26
49
  <div v-if="sessionsUsageLoading" class="usage-content-overlay" aria-live="polite">
27
50
  <span class="usage-spinner" aria-hidden="true"></span>
28
51
  </div>
29
52
 
30
- <div v-if="usageCurrentSessionStats" class="usage-current-session-bar">
31
- <span class="usage-current-session-dot"></span>
32
- <span class="usage-current-session-label">{{ usageCurrentSessionStats.label }}</span>
33
- <span class="usage-current-session-stat">{{ usageCurrentSessionStats.tokenLabel }} tokens</span>
34
- <span class="usage-current-session-stat">{{ usageCurrentSessionStats.apiDurationLabel }} API</span>
35
- <span class="usage-current-session-stat">{{ usageCurrentSessionStats.totalDurationLabel }} total</span>
36
- </div>
53
+ <!-- Hero 区域:合并当前会话条 + 主要指标 -->
54
+ <div class="usage-hero">
55
+ <div v-if="usageCurrentSessionStats" class="usage-hero-active">
56
+ <span class="usage-hero-active-dot"></span>
57
+ <span class="usage-hero-active-label">{{ usageCurrentSessionStats.label }}</span>
58
+ <span class="usage-hero-active-stat">{{ usageCurrentSessionStats.tokenLabel }} tokens</span>
59
+ <span class="usage-hero-active-stat">{{ usageCurrentSessionStats.apiDurationLabel }} API</span>
60
+ <span class="usage-hero-active-stat">{{ usageCurrentSessionStats.totalDurationLabel }} total</span>
61
+ </div>
37
62
 
38
- <div class="usage-summary-grid">
39
- <div v-for="card in sessionUsageSummaryCards" :key="card.key" class="usage-summary-card" :title="card.title || ''">
40
- <div class="usage-summary-card-value">{{ card.value }}</div>
41
- <div class="usage-summary-card-label">{{ card.label }}</div>
63
+ <div class="usage-hero-metrics">
64
+ <div class="usage-hero-main">{{ usageHeroMainValue }}</div>
65
+ <div class="usage-hero-sub">
66
+ <span>{{ usageHeroSubLabel }}</span>
67
+ <span v-if="usageHeroDelta" :class="['usage-hero-delta', usageHeroDeltaClass]">
68
+ {{ usageHeroDelta }}
69
+ </span>
70
+ </div>
42
71
  </div>
43
72
  </div>
44
73
 
45
- <section v-if="sessionUsageDaily && sessionUsageDaily.rows.length" class="usage-card">
46
- <div class="usage-card-head">
47
- <div class="usage-card-title">{{ t('usage.daily.title') }}</div>
48
- <div class="usage-card-subtitle">{{ t('usage.daily.subtitle') }}</div>
49
- </div>
50
- <div class="usage-daily-chart">
51
- <div v-for="day in sessionUsageDaily.rows" :key="day.key" class="usage-daily-bar-group" @click="selectSessionsUsageDay(day.key)" :class="{ active: sessionsUsageSelectedDay === day.key }">
52
- <div class="usage-daily-bar-stack">
53
- <div class="usage-daily-bar-fill" :style="{ height: day.tokenPercent + '%' }"></div>
54
- <div v-if="day.compareEnabled" class="usage-daily-bar-prev" :style="{ height: day.prevTokenPercent + '%' }"></div>
55
- </div>
56
- <div class="usage-daily-bar-label">{{ day.label }}</div>
74
+ <!-- 波浪图 -->
75
+ <section v-if="sessionUsageWave.points && sessionUsageWave.points.length" class="usage-wave-section">
76
+ <div class="usage-card-title">{{ t('usage.daily.title') }}</div>
77
+ <div class="usage-wave-container">
78
+ <svg class="usage-wave-chart" viewBox="0 0 800 140" preserveAspectRatio="none">
79
+ <defs>
80
+ <linearGradient :id="'wave-gradient-' + sessionsUsageTimeRange" x1="0" y1="0" x2="0" y2="1">
81
+ <stop offset="0%" :stop-color="'var(--color-brand)'" stop-opacity="0.35"/>
82
+ <stop offset="100%" :stop-color="'var(--color-brand)'" stop-opacity="0"/>
83
+ </linearGradient>
84
+ </defs>
85
+ <path :d="sessionUsageWave.areaPath" :fill="'url(#wave-gradient-' + sessionsUsageTimeRange + ')'" class="usage-wave-area"/>
86
+ <path :d="sessionUsageWave.linePath" fill="none" :stroke="'var(--color-brand)'" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="usage-wave-line"/>
87
+ <line v-if="sessionsUsageSelectedDay" x1="0" :x2="sessionUsageWave.width" :y1="sessionUsageWave.hoverY" :y2="sessionUsageWave.hoverY" stroke="currentColor" stroke-width="1" stroke-dasharray="4 4" opacity="0.5" class="usage-wave-hover-line"/>
88
+ <circle v-if="sessionsUsageSelectedDay" :cx="sessionUsageWave.hoverX" :cy="sessionUsageWave.hoverY" r="5" :fill="'var(--color-surface)'" :stroke="'var(--color-brand)'" stroke-width="2.5" class="usage-wave-hover-point"/>
89
+ </svg>
90
+ <div class="usage-wave-labels">
91
+ <span v-for="label in sessionUsageWave.labels" :key="label.key"
92
+ class="usage-wave-label"
93
+ :class="{ active: sessionsUsageSelectedDay === label.key }"
94
+ @click="selectSessionsUsageDay(label.key)">
95
+ {{ label.text }}
96
+ </span>
57
97
  </div>
58
98
  </div>
59
- <div class="usage-daily-legend">
60
- <span class="usage-daily-legend-item"><span class="usage-daily-legend-swatch current"></span>{{ t('usage.legend.current') }}</span>
61
- <span v-if="sessionsUsageCompareEnabled" class="usage-daily-legend-item"><span class="usage-daily-legend-swatch prev"></span>{{ t('usage.compare.prev') }}</span>
62
- </div>
63
- <template v-if="sessionsUsageSelectedDaySummary">
64
- <div class="usage-daydetail">
65
- <div class="usage-daydetail-header">
66
- <span class="usage-daydetail-date">{{ sessionsUsageSelectedDaySummary.dayKey }}</span>
67
- <span class="usage-daydetail-stats">{{ sessionsUsageSelectedDaySummary.sessionCount }} sessions · {{ sessionsUsageSelectedDaySummary.tokenLabel }} tokens</span>
68
- </div>
69
- <div v-if="sessionsUsageSelectedDaySummary.compareEnabled" class="usage-daydetail-compare">
70
- {{ t('usage.compare.prev') }} {{ sessionsUsageSelectedDaySummary.prevTokenLabel }} tokens · {{ t('usage.compare.delta') }} {{ sessionsUsageSelectedDaySummary.deltaTokenLabel }}
71
- </div>
99
+ <div v-if="sessionsUsageSelectedDaySummary" class="usage-daydetail">
100
+ <div class="usage-daydetail-header">
101
+ <span class="usage-daydetail-date">{{ sessionsUsageSelectedDaySummary.dayKey }}</span>
102
+ <span class="usage-daydetail-stats">{{ sessionsUsageSelectedDaySummary.sessionCount }} sessions · {{ sessionsUsageSelectedDaySummary.tokenLabel }} tokens</span>
103
+ </div>
104
+ <div v-if="sessionsUsageSelectedDaySummary.prevTokenLabel !== null" class="usage-daydetail-compare">
105
+ {{ t('usage.compare.prev') }} {{ sessionsUsageSelectedDaySummary.prevTokenLabel }} tokens · {{ t('usage.compare.delta') }} {{ sessionsUsageSelectedDaySummary.deltaTokenLabel }}
72
106
  </div>
73
- </template>
107
+ </div>
74
108
  </section>
75
109
 
76
110
  <div class="usage-chart-grid">
77
- <section class="usage-card usage-card-hourly-heatmap">
111
+ <!-- 热力图 -->
112
+ <section class="usage-card-hourly-heatmap">
78
113
  <div class="usage-card-title">{{ t('usage.hourlyHeatmap.title') }}</div>
79
114
  <div class="hourly-heatmap-wrapper">
80
115
  <div class="hourly-heatmap-header">
81
116
  <div class="hourly-heatmap-corner"></div>
82
117
  <div v-for="h in 24" :key="'hdr-' + h" class="hourly-heatmap-hour-label">{{ h - 1 }}</div>
83
118
  </div>
84
- <div v-for="row in sessionUsageHeatmap.rows" :key="row.weekday" class="hourly-heatmap-row">
119
+ <div v-for="row in sessionUsageHourlyHeatmap.rows" :key="row.weekday" class="hourly-heatmap-row">
85
120
  <div class="hourly-heatmap-weekday-label">{{ row.weekday }}</div>
86
121
  <div v-for="cell in row.cells" :key="'cell-' + row.weekday + '-' + cell.hour" :class="['hourly-heatmap-cell', 'level-' + cell.level]" :title="cell.tooltip"></div>
87
122
  </div>
88
123
  </div>
89
124
  <div class="hourly-heatmap-legend">
90
125
  <span class="hourly-heatmap-legend-label">{{ t('usage.hourlyHeatmap.legend.less') }}</span>
91
- <span class="hourly-heatmap-cell level-0"></span>
92
126
  <span class="hourly-heatmap-cell level-1"></span>
93
127
  <span class="hourly-heatmap-cell level-2"></span>
94
128
  <span class="hourly-heatmap-cell level-3"></span>
@@ -97,42 +131,51 @@
97
131
  </div>
98
132
  </section>
99
133
 
100
- <section class="usage-card">
101
- <div class="usage-card-title">{{ t('usage.paths.title') }}</div>
102
- <div v-if="!sessionUsageCharts.topPaths.length" class="usage-list-value">{{ t('usage.paths.empty') }}</div>
103
- <div v-else class="usage-list">
104
- <div v-for="item in sessionUsageCharts.topPaths" :key="item.path" class="usage-list-row">
105
- <div class="usage-list-path" :title="item.path">{{ item.path }}</div>
106
- <div class="usage-list-stat">{{ item.count }}</div>
107
- <div class="usage-progress"><div class="usage-progress-fill" :style="{ width: ((item.count / Math.max((sessionUsageCharts.topPaths.length ? sessionUsageCharts.topPaths[0].count : 1), 1)) * 100) + '%' }"></div></div>
108
- </div>
109
- </div>
110
- </section>
111
-
134
+ <!-- Top Sessions -->
112
135
  <section class="usage-card">
113
136
  <div class="usage-card-title">{{ t('usage.sessions.topDensity') }}</div>
114
137
  <div v-if="!sessionUsageCharts.topSessionsByMessages.length" class="usage-list-value">{{ t('usage.sessions.empty') }}</div>
115
- <div v-else class="usage-list">
116
- <div v-for="item in sessionUsageCharts.topSessionsByMessages" :key="item.key + '-dense'" class="usage-list-row">
117
- <div class="usage-list-title" :title="item.title">{{ item.title }}</div>
118
- <div class="usage-list-stat">{{ item.messageCount }}</div>
119
- <div class="usage-list-meta">{{ item.sourceLabel }} · {{ item.updatedAtLabel }}</div>
138
+ <div v-else class="usage-list-compact">
139
+ <div v-for="item in sessionUsageCharts.topSessionsByMessages" :key="item.key + '-dense'" class="usage-list-compact-item" @click="selectSession(item)" :title="item.title">
140
+ <span class="usage-list-bullet">·</span>
141
+ <div class="usage-list-compact-content">
142
+ <div class="usage-list-title">{{ item.title }}</div>
143
+ <div class="usage-list-meta">{{ item.messageCount }} msgs · {{ item.sourceLabel }} · {{ item.updatedAtLabel }}</div>
144
+ </div>
120
145
  </div>
121
146
  </div>
122
147
  </section>
123
148
 
149
+ <!-- Recent Activity -->
124
150
  <section class="usage-card">
125
151
  <div class="usage-card-title">{{ t('usage.recent.title') }}</div>
126
152
  <div v-if="!sessionUsageCharts.recentSessions.length" class="usage-list-value">{{ t('usage.sessions.empty') }}</div>
127
- <div v-else class="usage-list">
128
- <div v-for="item in sessionUsageCharts.recentSessions" :key="item.key" class="usage-list-row">
129
- <div class="usage-list-title" :title="item.title">{{ item.title }}</div>
130
- <span :class="['pill', item.source === 'codex' ? 'configured' : 'empty']">{{ item.sourceLabel }}</span>
131
- <div class="usage-list-meta">{{ item.messageCount }} msgs · {{ item.updatedAtLabel }}</div>
153
+ <div v-else class="usage-list-compact">
154
+ <div v-for="item in sessionUsageCharts.recentSessions" :key="item.key" class="usage-list-compact-item" @click="selectSession(item)">
155
+ <span class="usage-list-bullet">·</span>
156
+ <div class="usage-list-compact-content">
157
+ <div class="usage-list-title">{{ item.title }}</div>
158
+ <div class="usage-list-meta">{{ item.messageCount }} msgs · {{ item.sourceLabel }} · {{ item.updatedAtLabel }}</div>
159
+ </div>
160
+ </div>
161
+ </div>
162
+ </section>
163
+
164
+ <!-- Top Paths -->
165
+ <section class="usage-paths-section">
166
+ <div class="usage-card-title">{{ t('usage.paths.title') }}</div>
167
+ <div v-if="!sessionUsageCharts.topPaths.length" class="usage-list-value">{{ t('usage.paths.empty') }}</div>
168
+ <div v-else class="usage-list-paths">
169
+ <div v-for="(item, index) in sessionUsageCharts.topPaths" :key="item.path" class="usage-list-path-row">
170
+ <span class="usage-list-path-rank">{{ index + 1 }}</span>
171
+ <div class="usage-list-path-content">
172
+ <div class="usage-list-path" :title="item.path">{{ item.path }}</div>
173
+ <div class="usage-list-path-stat">{{ item.count }}</div>
174
+ </div>
132
175
  </div>
133
176
  </div>
134
177
  </section>
135
178
  </div>
136
179
  </div>
137
180
  </template>
138
- </div>
181
+ </div>