codexmate 0.0.33 → 0.0.36

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 (47) hide show
  1. package/cli/agents-files.js +6 -0
  2. package/cli/archive-helpers.js +11 -4
  3. package/cli/local-bridge.js +9 -4
  4. package/cli/openai-bridge.js +1 -1
  5. package/cli/update.js +11 -2
  6. package/cli.js +133 -64
  7. package/lib/cli-webhook.js +29 -1
  8. package/package.json +2 -1
  9. package/web-ui/app.js +37 -2
  10. package/web-ui/index.html +2 -1
  11. package/web-ui/logic.claude.mjs +4 -0
  12. package/web-ui/logic.sessions.mjs +6 -5
  13. package/web-ui/modules/app.computed.dashboard.mjs +4 -0
  14. package/web-ui/modules/app.computed.session.mjs +147 -6
  15. package/web-ui/modules/app.methods.claude-config.mjs +41 -0
  16. package/web-ui/modules/app.methods.codex-config.mjs +11 -3
  17. package/web-ui/modules/app.methods.navigation.mjs +32 -2
  18. package/web-ui/modules/app.methods.session-browser.mjs +12 -6
  19. package/web-ui/modules/app.methods.session-trash.mjs +30 -0
  20. package/web-ui/modules/app.methods.startup-claude.mjs +9 -0
  21. package/web-ui/modules/app.methods.webhook.mjs +8 -0
  22. package/web-ui/modules/i18n.dict.mjs +8 -0
  23. package/web-ui/modules/sessions-filters-url.mjs +65 -12
  24. package/web-ui/modules/skills.methods.mjs +31 -0
  25. package/web-ui/partials/index/layout-header.html +17 -12
  26. package/web-ui/partials/index/modal-webhook.html +42 -0
  27. package/web-ui/partials/index/modals-basic.html +50 -0
  28. package/web-ui/partials/index/panel-config-claude.html +13 -22
  29. package/web-ui/partials/index/panel-config-codex.html +8 -22
  30. package/web-ui/partials/index/panel-market.html +76 -149
  31. package/web-ui/partials/index/panel-sessions.html +2 -2
  32. package/web-ui/partials/index/panel-settings.html +119 -149
  33. package/web-ui/partials/index/panel-usage.html +115 -68
  34. package/web-ui/res/vue.runtime.global.prod.js +7 -0
  35. package/web-ui/res/web-ui-render.precompiled.js +7274 -0
  36. package/web-ui/session-helpers.mjs +15 -4
  37. package/web-ui/source-bundle.cjs +73 -1
  38. package/web-ui/styles/base-theme.css +10 -0
  39. package/web-ui/styles/bridge-pool.css +69 -0
  40. package/web-ui/styles/layout-shell.css +66 -27
  41. package/web-ui/styles/navigation-panels.css +8 -0
  42. package/web-ui/styles/responsive.css +50 -9
  43. package/web-ui/styles/sessions-usage.css +336 -319
  44. package/web-ui/styles/settings-panel.css +300 -234
  45. package/web-ui/styles/skills-market.css +294 -0
  46. package/web-ui/styles/titles-cards.css +14 -0
  47. package/web-ui/styles/webhook.css +38 -4
@@ -1,31 +1,37 @@
1
-  <!-- 设置面板 -->
1
+ <!-- 设置面板 -->
2
2
  <div
3
3
  v-show="mainTab === 'settings'"
4
4
  class="mode-content"
5
5
  id="panel-settings"
6
6
  role="tabpanel"
7
7
  :aria-labelledby="'tab-settings'">
8
- <div class="settings-subtabs-bar" role="tablist" :aria-label="t('settings.tabs.aria')">
9
- <button
10
- id="settings-tab-general"
11
- role="tab"
12
- aria-controls="settings-panel-general"
13
- :aria-selected="settingsTab === 'general'"
14
- :tabindex="settingsTab === 'general' ? 0 : -1"
15
- :class="['settings-subtab', { active: settingsTab === 'general' }]"
16
- @click="onSettingsTabClick('general')">
17
- {{ t('settings.tab.general') }}
18
- </button>
19
- <button
20
- id="settings-tab-data"
21
- role="tab"
22
- aria-controls="settings-panel-data"
23
- :aria-selected="settingsTab === 'data'"
24
- :tabindex="settingsTab === 'data' ? 0 : -1"
25
- :class="['settings-subtab', { active: settingsTab === 'data' }]"
26
- @click="onSettingsTabClick('data')">
27
- {{ t('settings.tab.data') }}
28
- </button>
8
+
9
+ <div class="settings-tab-header">
10
+ <div
11
+ class="segmented-control"
12
+ role="tablist"
13
+ :aria-label="t('settings.tabs.aria')">
14
+ <button
15
+ id="settings-tab-general"
16
+ type="button"
17
+ role="tab"
18
+ aria-controls="settings-panel-general"
19
+ :aria-selected="settingsTab === 'general'"
20
+ :tabindex="settingsTab === 'general' ? 0 : -1"
21
+ :class="['segmented-option', { active: settingsTab === 'general' }]"
22
+ @click="onSettingsTabClick('general')"
23
+ @keydown="onSettingsTabKeydown($event, 'general')">{{ t('settings.tab.general') }}</button>
24
+ <button
25
+ id="settings-tab-data"
26
+ type="button"
27
+ role="tab"
28
+ aria-controls="settings-panel-data"
29
+ :aria-selected="settingsTab === 'data'"
30
+ :tabindex="settingsTab === 'data' ? 0 : -1"
31
+ :class="['segmented-option', { active: settingsTab === 'data' }]"
32
+ @click="onSettingsTabClick('data')"
33
+ @keydown="onSettingsTabKeydown($event, 'data')">{{ t('settings.tab.data') }}</button>
34
+ </div>
29
35
  </div>
30
36
 
31
37
  <div
@@ -33,73 +39,60 @@
33
39
  id="settings-panel-general"
34
40
  role="tabpanel"
35
41
  aria-labelledby="settings-tab-general">
36
- <div class="settings-layout">
37
- <div class="settings-grid">
38
- <section class="settings-card settings-card--wide" :aria-label="t('settings.sharePrefix.title')">
39
- <div class="settings-card-header">
42
+ <div class="settings-grid">
43
+ <section class="settings-card" :aria-label="t('settings.sharePrefix.title')">
44
+ <div class="settings-card-main">
45
+ <div class="settings-card-content">
40
46
  <div class="settings-card-title">{{ t('settings.sharePrefix.title') }}</div>
41
- <div class="settings-card-meta">{{ t('settings.sharePrefix.meta') }}</div>
42
- </div>
43
- <div class="settings-card-body">
44
- <div class="settings-field-row">
45
- <label class="settings-field-label" for="settings-share-prefix">{{ t('settings.sharePrefix.label') }}</label>
46
- <select
47
- id="settings-share-prefix"
48
- class="model-select"
49
- :value="shareCommandPrefix"
50
- @change="setShareCommandPrefix($event.target.value)">
51
- <option value="npm start">npm start</option>
52
- <option value="codexmate">codexmate</option>
53
- </select>
54
- </div>
55
- <div class="settings-card-hint">
56
- {{ t('settings.sharePrefix.hint') }}
57
- </div>
47
+ <p class="settings-card-desc">{{ t('settings.sharePrefix.meta') }}</p>
48
+ <label class="selector-label" for="settings-share-prefix">{{ t('settings.sharePrefix.label') }}</label>
49
+ <select
50
+ id="settings-share-prefix"
51
+ class="model-select"
52
+ :value="shareCommandPrefix"
53
+ @change="setShareCommandPrefix($event.target.value)">
54
+ <option value="npm start">npm start</option>
55
+ <option value="codexmate">codexmate</option>
56
+ </select>
57
+ <p class="settings-card-hint">{{ t('settings.sharePrefix.hint') }}</p>
58
58
  </div>
59
- </section>
59
+ </div>
60
+ </section>
60
61
 
61
- <section class="settings-card settings-card--wide" :aria-label="t('settings.templateConfirm.title')">
62
- <div class="settings-card-header">
62
+ <section class="settings-card" :aria-label="t('settings.templateConfirm.title')">
63
+ <div class="settings-card-main">
64
+ <div class="settings-card-content">
63
65
  <div class="settings-card-title">{{ t('settings.templateConfirm.title') }}</div>
64
- <div class="settings-card-meta">{{ t('settings.templateConfirm.meta') }}</div>
65
- </div>
66
- <div class="settings-card-body">
67
- <label class="health-remote-toggle settings-toggle">
68
- <input
69
- type="checkbox"
70
- :checked="configTemplateDiffConfirmEnabled"
71
- @change="setConfigTemplateDiffConfirmEnabled($event.target.checked)">
66
+ <p class="settings-card-desc">{{ t('settings.templateConfirm.meta') }}</p>
67
+ <label class="settings-toggle-row">
68
+ <input type="checkbox" :checked="configTemplateDiffConfirmEnabled" @change="setConfigTemplateDiffConfirmEnabled($event.target.checked)">
69
+ <span class="toggle-track">
70
+ <span class="toggle-thumb"></span>
71
+ </span>
72
72
  <span>{{ t('settings.templateConfirm.toggle') }}</span>
73
73
  </label>
74
- <div class="settings-card-hint">
75
- {{ t('settings.templateConfirm.hint') }}
76
- </div>
74
+ <p class="settings-card-hint">{{ t('settings.templateConfirm.hint') }}</p>
77
75
  </div>
78
- </section>
76
+ </div>
77
+ </section>
79
78
 
80
- <section class="settings-card settings-card--wide" :aria-label="t('settings.webhook.title')">
81
- <div class="settings-card-header settings-card-header-row">
82
- <div>
83
- <div class="settings-card-title">Webhook</div>
84
- <div class="settings-card-meta">配置变更外发通知</div>
85
- </div>
86
- <span :class="webhookConfig.enabled ? 'status-on' : 'status-off'">{{ webhookConfig.enabled ? '● 已启用' : '○ 已禁用' }}</span>
87
- </div>
88
- <div class="settings-card-body">
89
- <div v-if="webhookConfig.url" class="webhook-readonly-row">
90
- <span class="webhook-readonly-label">URL</span>
91
- <code class="webhook-readonly-value">{{ webhookConfig.url }}</code>
92
- </div>
93
- <div class="webhook-readonly-row">
94
- <span class="webhook-readonly-label">事件</span>
95
- <div class="webhook-readonly-tags">
96
- <span v-for="ev in webhookConfig.events" :key="ev" class="webhook-event-tag">{{ ev }}</span>
97
- <span v-if="!webhookConfig.events.length" class="settings-card-hint">无</span>
98
- </div>
79
+ <section class="settings-card" :aria-label="'Webhook'">
80
+ <div class="settings-card-main">
81
+ <div class="settings-card-content">
82
+ <div class="settings-card-title">Webhook</div>
83
+ <p class="settings-card-desc">配置变更时外发通知</p>
84
+ <div class="webhook-status">
85
+ <span class="webhook-status-dot" :class="{ active: webhookConfig.enabled }"></span>
86
+ <span class="webhook-status-label">{{ webhookConfig.enabled ? '已启用' : '已禁用' }}</span>
87
+ <code v-if="webhookConfig.url" class="webhook-url">{{ webhookConfig.url }}</code>
99
88
  </div>
100
89
  </div>
101
- </section>
102
- </div>
90
+ </div>
91
+ <button class="settings-card-action" @click="openWebhookModal" :class="{ 'settings-card-action--active': webhookConfig.enabled }">
92
+ <span v-if="webhookConfig.enabled">{{ webhookConfig.url ? '编辑' : '配置' }}</span>
93
+ <span v-else>启用</span>
94
+ </button>
95
+ </section>
103
96
  </div>
104
97
  </div>
105
98
 
@@ -108,83 +101,60 @@
108
101
  id="settings-panel-data"
109
102
  role="tabpanel"
110
103
  aria-labelledby="settings-tab-data">
111
- <div class="settings-layout">
112
- <div class="settings-grid">
113
- <section class="settings-card settings-card--wide" :aria-label="t('settings.backup.title')">
114
- <div class="settings-card-header settings-card-header-row">
115
- <div>
116
- <div class="settings-card-title">{{ t('settings.backup.title') }}</div>
117
- <div class="settings-card-meta">{{ t('settings.backup.meta') }}</div>
118
- </div>
119
- </div>
120
- <div class="settings-card-body">
121
- <div class="settings-backup-grid">
122
- <div class="settings-backup-slot">
123
- <div class="settings-backup-slot-label">Claude</div>
124
- <div class="settings-actions">
125
- <button class="btn-tool" @click="downloadClaudeDirectory" :disabled="claudeDownloadLoading">
126
- {{ claudeDownloadLoading ? t('settings.backup.progress', { percent: claudeDownloadProgress }) : t('settings.backup.oneClickClaude') }}
127
- </button>
128
- <button class="btn-tool" @click="triggerClaudeImport" :disabled="claudeImportLoading">
129
- {{ claudeImportLoading ? t('settings.importing') : t('settings.backup.importClaude') }}
130
- </button>
131
- </div>
132
- <input ref="claudeImportInput" class="sr-only" type="file" accept=".zip" @change="handleClaudeImportChange">
133
- </div>
134
- <div class="settings-backup-slot">
135
- <div class="settings-backup-slot-label">Codex</div>
136
- <div class="settings-actions">
137
- <button class="btn-tool" @click="downloadCodexDirectory" :disabled="codexDownloadLoading">
138
- {{ codexDownloadLoading ? t('settings.backup.progress', { percent: codexDownloadProgress }) : t('settings.backup.oneClickCodex') }}
139
- </button>
140
- <button class="btn-tool" @click="triggerCodexImport" :disabled="codexImportLoading">
141
- {{ codexImportLoading ? t('settings.importing') : t('settings.backup.importCodex') }}
142
- </button>
143
- </div>
144
- <input ref="codexImportInput" class="sr-only" type="file" accept=".zip" @change="handleCodexImportChange">
145
- </div>
146
- </div>
104
+ <div class="settings-grid">
105
+ <section class="settings-card" :aria-label="t('settings.backup.title')">
106
+ <div class="settings-card-main">
107
+ <div class="settings-card-content">
108
+ <div class="settings-card-title">{{ t('settings.backup.title') }}</div>
109
+ <p class="settings-card-desc">{{ t('settings.backup.meta') }}</p>
147
110
  </div>
148
- </section>
111
+ </div>
112
+ <div class="settings-card-actions">
113
+ <button class="settings-card-action" @click="downloadClaudeDirectory" :disabled="claudeDownloadLoading">
114
+ <span>{{ claudeDownloadLoading ? t('settings.importing') : t('settings.backup.oneClickClaude') }}</span>
115
+ </button>
116
+ <button class="settings-card-action" @click="downloadCodexDirectory" :disabled="codexDownloadLoading">
117
+ <span>{{ codexDownloadLoading ? t('settings.importing') : t('settings.backup.oneClickCodex') }}</span>
118
+ </button>
119
+ </div>
120
+ <input ref="claudeImportInput" class="sr-only" type="file" accept=".zip" @change="handleClaudeImportChange">
121
+ <input ref="codexImportInput" class="sr-only" type="file" accept=".zip" @change="handleCodexImportChange">
122
+ </section>
149
123
 
150
- <section class="settings-card settings-card--wide" :aria-label="t('settings.trashConfig.title')">
151
- <div class="settings-card-header settings-card-header-row">
152
- <div>
153
- <div class="settings-card-title">{{ t('settings.trashConfig.title') }}</div>
154
- <div class="settings-card-meta">{{ t('settings.trashConfig.meta') }}</div>
155
- </div>
156
- </div>
157
- <div class="settings-card-body">
158
- <div class="settings-trash-config-grid">
159
- <label class="settings-toggle">
160
- <input type="checkbox" :checked="sessionTrashEnabled" @change="setSessionTrashEnabled($event.target.checked)">
161
- <span>{{ t('settings.deleteBehavior.toggle') }}</span>
162
- </label>
163
- <label class="settings-retention-row">
164
- <span>{{ t('settings.trash.retentionLabel') }}</span>
165
- <input type="number" min="1" max="365" :value="sessionTrashRetentionDays" @change="setSessionTrashRetentionDays(Number($event.target.value))" class="settings-retention-input" />
166
- </label>
124
+ <section class="settings-card" :aria-label="t('settings.trashConfig.title')">
125
+ <div class="settings-card-main">
126
+ <div class="settings-card-content">
127
+ <div class="settings-card-title">{{ t('settings.trashConfig.title') }}</div>
128
+ <p class="settings-card-desc">{{ t('settings.trashConfig.meta') }}</p>
129
+ <label class="settings-toggle-row">
130
+ <input type="checkbox" :checked="sessionTrashEnabled" @change="setSessionTrashEnabled($event.target.checked)">
131
+ <span class="toggle-track">
132
+ <span class="toggle-thumb"></span>
133
+ </span>
134
+ <span>{{ t('settings.deleteBehavior.toggle') }}</span>
135
+ </label>
136
+ <div class="settings-retention">
137
+ <label for="settings-trash-retention-days">{{ t('settings.trash.retentionLabel') }}</label>
138
+ <input id="settings-trash-retention-days" type="number" min="1" max="365" :value="sessionTrashRetentionDays" @change="setSessionTrashRetentionDays(Number($event.target.value))" class="settings-retention-input" />
139
+ <span>天</span>
167
140
  </div>
168
- <div class="settings-card-hint">{{ t('settings.trash.retentionHint') }}</div>
141
+ <p class="settings-card-hint">{{ t('settings.trash.retentionHint') }}</p>
169
142
  </div>
170
- </section>
171
-
143
+ </div>
144
+ </section>
172
145
 
173
- <section class="settings-card settings-card--wide settings-card--danger" :aria-label="t('settings.reset.title')">
174
- <div class="settings-card-header">
146
+ <section class="settings-card settings-card--destructive" :aria-label="t('settings.reset.title')">
147
+ <div class="settings-card-main">
148
+ <div class="settings-card-content">
175
149
  <div class="settings-card-title">{{ t('settings.reset.title') }}</div>
176
- <div class="settings-card-meta">{{ t('settings.reset.meta') }}</div>
177
- </div>
178
- <div class="settings-card-body">
179
- <div class="settings-card-hint">{{ t('settings.reset.hint') }}</div>
180
- <div class="settings-actions">
181
- <button class="btn-tool" @click="resetConfig" :disabled="resetConfigLoading || loading || !!initError">
182
- {{ resetConfigLoading ? t('settings.reset.loading') : t('settings.reset.button') }}
183
- </button>
184
- </div>
150
+ <p class="settings-card-desc">{{ t('settings.reset.meta') }}</p>
151
+ <p class="settings-card-hint">{{ t('settings.reset.hint') }}</p>
185
152
  </div>
186
- </section>
187
- </div>
153
+ </div>
154
+ <button class="settings-card-action settings-card-action--danger" @click="resetConfig" :disabled="resetConfigLoading || loading || !!initError">
155
+ {{ resetConfigLoading ? t('settings.reset.loading') : t('settings.reset.button') }}
156
+ </button>
157
+ </section>
188
158
  </div>
189
159
  </div>
190
160
  </div>
@@ -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,122 @@
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-card 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.3"/>
82
+ <stop offset="100%" :stop-color="'var(--color-brand)'" stop-opacity="0"/>
83
+ </linearGradient>
84
+ </defs>
85
+ <!-- 填充区域 -->
86
+ <path :d="sessionUsageWave.areaPath" :fill="'url(#wave-gradient-' + sessionsUsageTimeRange + ')'" class="usage-wave-area"/>
87
+ <!-- 曲线 -->
88
+ <path :d="sessionUsageWave.linePath" fill="none" :stroke="'var(--color-brand)'" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="usage-wave-line"/>
89
+ <!-- 悬停指示线 -->
90
+ <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.4" class="usage-wave-hover-line"/>
91
+ <!-- 悬停点 -->
92
+ <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"/>
93
+ </svg>
94
+ <div class="usage-wave-labels">
95
+ <span v-for="label in sessionUsageWave.labels" :key="label.key"
96
+ class="usage-wave-label"
97
+ :class="{ active: sessionsUsageSelectedDay === label.key }"
98
+ @click="selectSessionsUsageDay(label.key)">
99
+ {{ label.text }}
100
+ </span>
57
101
  </div>
58
102
  </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>
103
+ <div v-if="sessionsUsageSelectedDaySummary" class="usage-daydetail">
104
+ <div class="usage-daydetail-header">
105
+ <span class="usage-daydetail-date">{{ sessionsUsageSelectedDaySummary.dayKey }}</span>
106
+ <span class="usage-daydetail-stats">{{ sessionsUsageSelectedDaySummary.sessionCount }} sessions · {{ sessionsUsageSelectedDaySummary.tokenLabel }} tokens</span>
107
+ </div>
108
+ <div v-if="sessionsUsageSelectedDaySummary.prevTokenLabel !== null" class="usage-daydetail-compare">
109
+ {{ t('usage.compare.prev') }} {{ sessionsUsageSelectedDaySummary.prevTokenLabel }} tokens · {{ t('usage.compare.delta') }} {{ sessionsUsageSelectedDaySummary.deltaTokenLabel }}
72
110
  </div>
73
- </template>
111
+ </div>
74
112
  </section>
75
113
 
76
114
  <div class="usage-chart-grid">
77
- <section class="usage-card usage-card-hourly-heatmap">
115
+ <!-- 热力图:垂直活动条 -->
116
+ <section class="usage-card usage-hourly-heatmap">
78
117
  <div class="usage-card-title">{{ t('usage.hourlyHeatmap.title') }}</div>
79
118
  <div class="hourly-heatmap-wrapper">
80
119
  <div class="hourly-heatmap-header">
81
120
  <div class="hourly-heatmap-corner"></div>
82
121
  <div v-for="h in 24" :key="'hdr-' + h" class="hourly-heatmap-hour-label">{{ h - 1 }}</div>
83
122
  </div>
84
- <div v-for="row in sessionUsageHeatmap.rows" :key="row.weekday" class="hourly-heatmap-row">
123
+ <div v-for="row in sessionUsageHourlyHeatmap.rows" :key="row.weekday" class="hourly-heatmap-row">
85
124
  <div class="hourly-heatmap-weekday-label">{{ row.weekday }}</div>
86
125
  <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
126
  </div>
88
127
  </div>
89
128
  <div class="hourly-heatmap-legend">
90
129
  <span class="hourly-heatmap-legend-label">{{ t('usage.hourlyHeatmap.legend.less') }}</span>
91
- <span class="hourly-heatmap-cell level-0"></span>
92
130
  <span class="hourly-heatmap-cell level-1"></span>
93
131
  <span class="hourly-heatmap-cell level-2"></span>
94
132
  <span class="hourly-heatmap-cell level-3"></span>
@@ -97,42 +135,51 @@
97
135
  </div>
98
136
  </section>
99
137
 
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
-
138
+ <!-- Top Sessions -->
112
139
  <section class="usage-card">
113
140
  <div class="usage-card-title">{{ t('usage.sessions.topDensity') }}</div>
114
141
  <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>
142
+ <div v-else class="usage-list-compact">
143
+ <div v-for="item in sessionUsageCharts.topSessionsByMessages" :key="item.key + '-dense'" class="usage-list-compact-item" @click="selectSession(item)" :title="item.title">
144
+ <span class="usage-list-bullet">·</span>
145
+ <div class="usage-list-compact-content">
146
+ <div class="usage-list-title">{{ item.title }}</div>
147
+ <div class="usage-list-meta">{{ item.messageCount }} msgs · {{ item.sourceLabel }} · {{ item.updatedAtLabel }}</div>
148
+ </div>
120
149
  </div>
121
150
  </div>
122
151
  </section>
123
152
 
153
+ <!-- Recent Activity -->
124
154
  <section class="usage-card">
125
155
  <div class="usage-card-title">{{ t('usage.recent.title') }}</div>
126
156
  <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>
157
+ <div v-else class="usage-list-compact">
158
+ <div v-for="item in sessionUsageCharts.recentSessions" :key="item.key" class="usage-list-compact-item" @click="selectSession(item)">
159
+ <span class="usage-list-bullet">·</span>
160
+ <div class="usage-list-compact-content">
161
+ <div class="usage-list-title">{{ item.title }}</div>
162
+ <div class="usage-list-meta">{{ item.messageCount }} msgs · {{ item.sourceLabel }} · {{ item.updatedAtLabel }}</div>
163
+ </div>
164
+ </div>
165
+ </div>
166
+ </section>
167
+
168
+ <!-- Top Paths -->
169
+ <section class="usage-card usage-paths-section">
170
+ <div class="usage-card-title">{{ t('usage.paths.title') }}</div>
171
+ <div v-if="!sessionUsageCharts.topPaths.length" class="usage-list-value">{{ t('usage.paths.empty') }}</div>
172
+ <div v-else class="usage-list-paths">
173
+ <div v-for="(item, index) in sessionUsageCharts.topPaths" :key="item.path" class="usage-list-path-row">
174
+ <span class="usage-list-path-rank">{{ index + 1 }}</span>
175
+ <div class="usage-list-path-content">
176
+ <div class="usage-list-path" :title="item.path">{{ item.path }}</div>
177
+ <div class="usage-list-path-stat">{{ item.count }}</div>
178
+ </div>
132
179
  </div>
133
180
  </div>
134
181
  </section>
135
182
  </div>
136
183
  </div>
137
184
  </template>
138
- </div>
185
+ </div>