codexmate 0.0.25 → 0.0.27

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 (35) hide show
  1. package/README.md +11 -3
  2. package/README.zh.md +10 -2
  3. package/cli/builtin-proxy.js +315 -95
  4. package/cli/openai-bridge.js +99 -5
  5. package/cli/session-convert-args.js +65 -0
  6. package/cli/session-convert-io.js +82 -0
  7. package/cli/session-convert.js +43 -0
  8. package/cli.js +547 -32
  9. package/package.json +74 -74
  10. package/web-ui/app.js +24 -2
  11. package/web-ui/logic.session-convert.mjs +70 -0
  12. package/web-ui/logic.sessions.mjs +151 -0
  13. package/web-ui/modules/app.computed.dashboard.mjs +44 -1
  14. package/web-ui/modules/app.computed.session.mjs +336 -12
  15. package/web-ui/modules/app.methods.claude-config.mjs +11 -1
  16. package/web-ui/modules/app.methods.codex-config.mjs +76 -0
  17. package/web-ui/modules/app.methods.navigation.mjs +51 -3
  18. package/web-ui/modules/app.methods.session-actions.mjs +55 -3
  19. package/web-ui/modules/app.methods.session-browser.mjs +270 -3
  20. package/web-ui/modules/app.methods.session-timeline.mjs +34 -3
  21. package/web-ui/modules/app.methods.session-trash.mjs +16 -1
  22. package/web-ui/modules/app.methods.startup-claude.mjs +234 -125
  23. package/web-ui/modules/i18n.dict.mjs +76 -0
  24. package/web-ui/partials/index/panel-config-claude.html +12 -4
  25. package/web-ui/partials/index/panel-sessions.html +33 -10
  26. package/web-ui/partials/index/panel-settings.html +16 -0
  27. package/web-ui/partials/index/panel-usage.html +95 -85
  28. package/web-ui/session-helpers.mjs +3 -0
  29. package/web-ui/styles/base-theme.css +29 -25
  30. package/web-ui/styles/layout-shell.css +1 -1
  31. package/web-ui/styles/navigation-panels.css +9 -9
  32. package/web-ui/styles/sessions-list.css +17 -0
  33. package/web-ui/styles/sessions-toolbar-trash.css +62 -0
  34. package/web-ui/styles/sessions-usage.css +211 -83
  35. package/web-ui/styles/settings-panel.css +19 -0
@@ -81,6 +81,14 @@
81
81
  <option value="30d">{{ t('sessions.time.30d') }}</option>
82
82
  <option value="90d">{{ t('sessions.time.90d') }}</option>
83
83
  </select>
84
+ <select
85
+ class="session-time-select"
86
+ v-model="sessionSortMode"
87
+ @change="onSessionSortChange"
88
+ :disabled="sessionsLoading">
89
+ <option value="time">{{ t('sessions.sort.time') }}</option>
90
+ <option value="hot">{{ t('sessions.sort.hot') }}</option>
91
+ </select>
84
92
  <button class="btn-tool btn-tool-compact" type="button" @click="copySessionsFilterShareUrl" :disabled="sessionsLoading">
85
93
  {{ t('sessions.filters.copyLink') }}
86
94
  </button>
@@ -99,6 +107,19 @@
99
107
  {{ t('sessions.resumeYolo') }}
100
108
  </label>
101
109
  </div>
110
+ <div v-if="hasActiveSessionFilters()" class="session-filter-chips" :aria-label="t('sessions.filters.copyLink')">
111
+ <button
112
+ v-for="chip in getSessionFilterChips()"
113
+ :key="'chip-' + chip.key"
114
+ type="button"
115
+ class="session-filter-chip"
116
+ @click="clearSessionFilterChip(chip.key)"
117
+ :title="`${chip.title}: ${chip.value}`">
118
+ <span class="session-filter-chip-title">{{ chip.title }}</span>
119
+ <span class="session-filter-chip-value">{{ chip.value }}</span>
120
+ <span class="session-filter-chip-close">×</span>
121
+ </button>
122
+ </div>
102
123
  </div>
103
124
 
104
125
  <div v-if="sessionsLoading" class="state-message">
@@ -118,7 +139,7 @@
118
139
  <div
119
140
  v-for="session in visibleSessionsList"
120
141
  :key="session.source + '-' + session.sessionId + '-' + session.filePath"
121
- v-memo="[activeSessionExportKey === getSessionExportKey(session), session.messageCount, session.updatedAt, session.title, session.sourceLabel, isSessionPinned(session), sessionsLoading]"
142
+ v-memo="[activeSessionExportKey === getSessionExportKey(session), session.messageCount, session.updatedAt, session.title, session.sourceLabel, session.cwd, isSessionPinned(session), sessionsLoading]"
122
143
  :class="[
123
144
  'session-item',
124
145
  {
@@ -135,7 +156,7 @@
135
156
  <div class="session-item-header">
136
157
  <div class="session-item-main">
137
158
  <div class="session-item-title">{{ session.title || session.sessionId }}</div>
138
- <span class="session-count-badge">{{ session.messageCount == null ? 0 : session.messageCount }}</span>
159
+ <span class="session-count-badge">{{ session.messageCount == null ? '...' : session.messageCount }}</span>
139
160
  </div>
140
161
  <div class="session-item-actions">
141
162
  <button
@@ -169,6 +190,8 @@
169
190
  <div class="session-item-meta">
170
191
  <span class="session-source">{{ session.sourceLabel }}</span>
171
192
  <span class="session-item-time">{{ session.updatedAt || t('sessions.unknownTime') }}</span>
193
+ <span v-if="getSessionHotLabel(session)" class="session-item-hot">{{ getSessionHotLabel(session) }}</span>
194
+ <span v-if="session.cwd" class="session-item-cwd session-item-sub">{{ session.cwd }}</span>
172
195
  </div>
173
196
  </div>
174
197
  </div>
@@ -205,6 +228,12 @@
205
228
  :disabled="!activeSession || sessionExporting[getSessionExportKey(activeSession)]">
206
229
  {{ (activeSession && sessionExporting[getSessionExportKey(activeSession)]) ? t('sessions.preview.exporting') : t('sessions.preview.export') }}
207
230
  </button>
231
+ <button
232
+ class="btn-session-export"
233
+ @click="convertSession(activeSession)"
234
+ :disabled="true">
235
+ {{ (activeSession && sessionConverting[getSessionExportKey(activeSession)]) ? t('sessions.preview.converting') : t('sessions.preview.convert') }}
236
+ </button>
208
237
  <button
209
238
  class="btn-session-open"
210
239
  @click="openSessionStandalone(activeSession)"
@@ -247,15 +276,9 @@
247
276
  class="session-item-sub session-item-wrap"
248
277
  style="display:flex;align-items:center;justify-content:space-between;gap:8px;">
249
278
  <span>{{ t('sessions.preview.shownCount', { shown: activeSessionVisibleMessages.length, total: activeSessionMessages.length }) }}</span>
250
- <button class="btn-session-refresh" @click="loadMoreSessionMessages()" :disabled="sessionDetailLoading || sessionPreviewLoadingMore">
251
- {{ sessionPreviewLoadingMore ? t('sessions.preview.loading') : t('sessions.preview.loadMore', { remain: sessionPreviewRemainingCount }) }}
252
- </button>
253
- </div>
254
- <div
255
- v-if="sessionPreviewLoadingMore"
256
- class="session-item-sub session-item-wrap">
257
- {{ t('sessions.preview.loadingMore') }}
279
+ <span>{{ t('sessions.preview.loadMore', { remain: sessionPreviewRemainingCount }) }}</span>
258
280
  </div>
281
+ <div v-if="sessionPreviewLoadingMore" class="session-item-sub session-item-wrap">{{ t('sessions.preview.loadingMore') }}</div>
259
282
  <div
260
283
  v-for="(msg, idx) in activeSessionVisibleMessages"
261
284
  :key="getRecordRenderKey(msg, idx)"
@@ -141,6 +141,22 @@
141
141
  </div>
142
142
  </section>
143
143
 
144
+ <section class="settings-card settings-card--wide" :aria-label="t('settings.trash.retention')">
145
+ <div class="settings-card-header">
146
+ <div class="settings-card-title">{{ t('settings.trash.retention') }}</div>
147
+ <div class="settings-card-meta">{{ t('settings.trash.retentionMeta') }}</div>
148
+ </div>
149
+ <div class="settings-card-body">
150
+ <label class="settings-retention-row">
151
+ <span>{{ t('settings.trash.retentionLabel') }}</span>
152
+ <input type="number" min="1" max="365" :value="sessionTrashRetentionDays" @change="setSessionTrashRetentionDays(Number($event.target.value))" class="settings-retention-input" />
153
+ </label>
154
+ <div class="settings-card-hint">
155
+ {{ t('settings.trash.retentionHint') }}
156
+ </div>
157
+ </div>
158
+ </section>
159
+
144
160
  <section class="settings-card settings-card--wide" :aria-label="t('settings.trash.title')">
145
161
  <div class="settings-card-header settings-card-header-row">
146
162
  <div>
@@ -13,6 +13,14 @@
13
13
  <button type="button" class="usage-range-btn" :class="{ active: sessionsUsageTimeRange === '7d' }" @click="setSessionsUsageTimeRange('7d')">{{ t('usage.range.7d') }}</button>
14
14
  <button type="button" class="usage-range-btn" :class="{ active: sessionsUsageTimeRange === '30d' }" @click="setSessionsUsageTimeRange('30d')">{{ t('usage.range.30d') }}</button>
15
15
  <button type="button" class="usage-range-btn" :class="{ active: sessionsUsageTimeRange === 'all' }" @click="setSessionsUsageTimeRange('all')">{{ t('usage.range.all') }}</button>
16
+ <button
17
+ type="button"
18
+ class="usage-range-btn"
19
+ :class="{ active: sessionsUsageCompareEnabled }"
20
+ @click="toggleSessionsUsageCompare"
21
+ :disabled="sessionsUsageTimeRange === 'all'">
22
+ {{ t('usage.compare.toggle') }}
23
+ </button>
16
24
  <button type="button" class="usage-range-btn" @click="loadSessionsUsage({ forceRefresh: true, range: sessionsUsageTimeRange })" :disabled="sessionsUsageLoading">{{ sessionsUsageLoading ? t('usage.refreshing') : t('usage.refresh') }}</button>
17
25
  <button type="button" class="usage-range-btn" @click="switchMainTab('dashboard')">{{ t('dashboard.doctor.title') }}</button>
18
26
  </div>
@@ -79,97 +87,69 @@
79
87
  </div>
80
88
 
81
89
  <div class="usage-chart-grid">
82
- <section class="usage-card usage-card-wide usage-card-daily">
90
+ <section class="usage-card usage-card-daydetail">
83
91
  <div class="usage-card-head">
84
92
  <div>
85
- <div class="usage-card-title">{{ t('usage.daily.title') }}</div>
86
- <div class="usage-card-subtitle">{{ t('usage.daily.subtitle') }}</div>
93
+ <div class="usage-card-title">{{ t('usage.dayDetail.title', { day: sessionsUsageSelectedDaySummary ? sessionsUsageSelectedDaySummary.dayKey : '--' }) }}</div>
94
+ <div class="usage-card-subtitle">{{ t('usage.dayDetail.subtitle') }}</div>
95
+ </div>
96
+ <div class="usage-daydetail-controls">
97
+ <select
98
+ class="usage-daydetail-select"
99
+ :value="sessionsUsageSelectedDayKey || (sessionUsageDailyTableRows[0] && sessionUsageDailyTableRows[0].key) || ''"
100
+ @change="selectSessionsUsageDay(($event.target && $event.target.value) ? String($event.target.value) : '')">
101
+ <option v-for="day in sessionUsageDailyTableRows" :key="'day-select-' + day.key" :value="day.key">{{ day.key }}</option>
102
+ </select>
103
+ <button
104
+ type="button"
105
+ class="btn-tool btn-tool-compact"
106
+ :disabled="!sessionsUsageSelectedDaySummary"
107
+ @click="clearSessionsUsageDay">{{ t('usage.dayDetail.clear') }}</button>
87
108
  </div>
88
- <div class="usage-card-kicker">{{ sessionsUsageTimeRange === 'all' ? t('usage.range.kicker.all') : (sessionsUsageTimeRange === '30d' ? t('usage.range.kicker.30d') : t('usage.range.kicker.7d')) }}</div>
89
- </div>
90
-
91
- <div class="usage-daily-legend">
92
- <span><span class="usage-legend-dot usage-daily-dot usage-daily-dot-tokens"></span>{{ t('usage.legend.tokens') }}</span>
93
- <span><span class="usage-legend-dot usage-daily-dot usage-daily-dot-cost"></span>{{ t('usage.legend.cost') }}</span>
94
109
  </div>
95
-
96
- <div class="usage-daily-bars usage-bars">
97
- <div v-for="day in (sessionUsageDaily && sessionUsageDaily.rows ? sessionUsageDaily.rows : [])" :key="day.key" class="usage-bar-group">
98
- <div class="usage-daily-bar-stack" :title="`${day.key} · ${day.tokenTitle} token · ${day.costTitle}`">
99
- <div class="usage-daily-bar usage-daily-bar-tokens" :style="{ height: day.tokenPercent + '%' }"></div>
100
- <div class="usage-daily-bar usage-daily-bar-cost" :style="{ height: day.costPercent + '%' }"></div>
110
+ <template v-if="sessionsUsageSelectedDaySummary">
111
+ <div class="usage-daydetail-grid">
112
+ <div class="usage-daydetail-metric">
113
+ <div class="usage-daydetail-label">{{ t('usage.table.sessions') }}</div>
114
+ <div class="usage-daydetail-value">{{ sessionsUsageSelectedDaySummary.sessionCount }}</div>
115
+ </div>
116
+ <div class="usage-daydetail-metric">
117
+ <div class="usage-daydetail-label">{{ t('usage.table.messages') }}</div>
118
+ <div class="usage-daydetail-value">{{ sessionsUsageSelectedDaySummary.messageCount }}</div>
119
+ </div>
120
+ <div class="usage-daydetail-metric">
121
+ <div class="usage-daydetail-label">{{ t('usage.table.tokens') }}</div>
122
+ <div class="usage-daydetail-value mono">{{ sessionsUsageSelectedDaySummary.tokenLabel }}</div>
123
+ <div v-if="sessionsUsageSelectedDaySummary.compareEnabled" class="usage-daydetail-sub mono">
124
+ {{ t('usage.compare.prev') }} {{ sessionsUsageSelectedDaySummary.prevTokenLabel }} · {{ t('usage.compare.delta') }} {{ sessionsUsageSelectedDaySummary.deltaTokenLabel }}
125
+ </div>
126
+ </div>
127
+ <div class="usage-daydetail-metric">
128
+ <div class="usage-daydetail-label">{{ t('usage.table.cost') }}</div>
129
+ <div class="usage-daydetail-value mono">{{ sessionsUsageSelectedDaySummary.costLabel }}</div>
130
+ <div v-if="sessionsUsageSelectedDaySummary.compareEnabled" class="usage-daydetail-sub mono">
131
+ {{ t('usage.compare.prev') }} {{ sessionsUsageSelectedDaySummary.prevCostLabel }} · {{ t('usage.compare.delta') }} {{ sessionsUsageSelectedDaySummary.deltaCostLabel }}
132
+ </div>
101
133
  </div>
102
- <div class="usage-bar-label">{{ day.label }}</div>
103
134
  </div>
104
- </div>
105
-
106
- <div class="usage-daily-table-wrap">
107
- <table class="usage-daily-table">
108
- <thead>
109
- <tr>
110
- <th>{{ t('usage.table.date') }}</th>
111
- <th class="right">{{ t('usage.table.sessions') }}</th>
112
- <th class="right">{{ t('usage.table.messages') }}</th>
113
- <th class="right">{{ t('usage.table.tokens') }}</th>
114
- <th class="right">{{ t('usage.table.cost') }}</th>
115
- </tr>
116
- </thead>
117
- <tbody>
118
- <tr v-for="day in sessionUsageDailyTableRows" :key="day.key">
119
- <td class="mono">{{ day.key }}</td>
120
- <td class="right">{{ day.sessionCount }}</td>
121
- <td class="right">{{ day.messageCount }}</td>
122
- <td
123
- class="right mono usage-copyable"
124
- role="button"
125
- tabindex="0"
126
- :title="t('usage.copyTitle', { value: day.tokenTitle })"
127
- @click="(String(day.tokenTitle || '').trim())
128
- ? ((typeof fallbackCopyText === 'function' && fallbackCopyText(String(day.tokenTitle || '').trim()))
129
- ? showMessage(t('usage.copyTokenDay', { day: day.key }), 'success')
130
- : showMessage(t('usage.copyFail'), 'error'))
131
- : showMessage(t('usage.copyNone'), 'info')"
132
- @keydown.enter.prevent="(String(day.tokenTitle || '').trim())
133
- ? ((typeof fallbackCopyText === 'function' && fallbackCopyText(String(day.tokenTitle || '').trim()))
134
- ? showMessage(t('usage.copyTokenDay', { day: day.key }), 'success')
135
- : showMessage(t('usage.copyFail'), 'error'))
136
- : showMessage(t('usage.copyNone'), 'info')"
137
- @keydown.space.prevent="(String(day.tokenTitle || '').trim())
138
- ? ((typeof fallbackCopyText === 'function' && fallbackCopyText(String(day.tokenTitle || '').trim()))
139
- ? showMessage(t('usage.copyTokenDay', { day: day.key }), 'success')
140
- : showMessage(t('usage.copyFail'), 'error'))
141
- : showMessage(t('usage.copyNone'), 'info')">
142
- {{ day.tokenLabel }}
143
- </td>
144
- <td
145
- class="right mono usage-copyable"
146
- role="button"
147
- tabindex="0"
148
- :title="t('usage.copyTitle', { value: day.costTitle })"
149
- @click="(String(day.costTitle || '').trim())
150
- ? ((typeof fallbackCopyText === 'function' && fallbackCopyText(String(day.costTitle || '').trim()))
151
- ? showMessage(t('usage.copyCostDay', { day: day.key }), 'success')
152
- : showMessage(t('usage.copyFail'), 'error'))
153
- : showMessage(t('usage.copyNone'), 'info')"
154
- @keydown.enter.prevent="(String(day.costTitle || '').trim())
155
- ? ((typeof fallbackCopyText === 'function' && fallbackCopyText(String(day.costTitle || '').trim()))
156
- ? showMessage(t('usage.copyCostDay', { day: day.key }), 'success')
157
- : showMessage(t('usage.copyFail'), 'error'))
158
- : showMessage(t('usage.copyNone'), 'info')"
159
- @keydown.space.prevent="(String(day.costTitle || '').trim())
160
- ? ((typeof fallbackCopyText === 'function' && fallbackCopyText(String(day.costTitle || '').trim()))
161
- ? showMessage(t('usage.copyCostDay', { day: day.key }), 'success')
162
- : showMessage(t('usage.copyFail'), 'error'))
163
- : showMessage(t('usage.copyNone'), 'info')">
164
- {{ day.costLabel }}
165
- </td>
166
- </tr>
167
- </tbody>
168
- </table>
169
- </div>
170
-
171
- <div class="usage-daily-note">
172
- {{ t('usage.daily.note') }}
135
+ <div class="usage-daydetail-section">
136
+ <div class="usage-daydetail-section-title">{{ t('usage.dayDetail.topSessions') }}</div>
137
+ <div class="usage-daydetail-list">
138
+ <div v-for="item in sessionsUsageSelectedDaySummary.topSessions" :key="item.key" class="usage-daydetail-row">
139
+ <div class="usage-daydetail-row-title">{{ item.title }}</div>
140
+ <div class="usage-daydetail-row-meta mono">{{ item.messageCountLabel }}</div>
141
+ </div>
142
+ </div>
143
+ </div>
144
+ <div v-if="sessionsUsageSelectedDaySummary.topModels.length" class="usage-daydetail-section">
145
+ <div class="usage-daydetail-section-title">{{ t('usage.dayDetail.topModels') }}</div>
146
+ <div class="usage-daydetail-models">
147
+ <span v-for="model in sessionsUsageSelectedDaySummary.topModels" :key="model.key" class="usage-daydetail-model">{{ model.label }}</span>
148
+ </div>
149
+ </div>
150
+ </template>
151
+ <div v-else class="usage-daydetail-empty">
152
+ {{ t('usage.dayDetail.empty') }}
173
153
  </div>
174
154
  </section>
175
155
 
@@ -307,6 +287,36 @@
307
287
  </div>
308
288
  </section>
309
289
 
290
+ <section class="usage-card usage-card-hourly-heatmap">
291
+ <div class="usage-card-title">{{ t('usage.hourlyHeatmap.title') }}</div>
292
+ <div class="usage-card-subtitle">{{ t('usage.hourlyHeatmap.subtitle') }}</div>
293
+ <div class="hourly-heatmap-wrapper">
294
+ <div class="hourly-heatmap-header">
295
+ <div class="hourly-heatmap-corner"></div>
296
+ <div v-for="h in sessionUsageHourlyHeatmap.hourLabels" :key="'hh-' + h" class="hourly-heatmap-hour-label">{{ h }}</div>
297
+ </div>
298
+ <div v-for="(row, dayIndex) in sessionUsageHourlyHeatmap.rows" :key="'hd-' + dayIndex" class="hourly-heatmap-row">
299
+ <div class="hourly-heatmap-weekday-label">{{ row.weekday }}</div>
300
+ <div
301
+ v-for="(cell, hourIndex) in row.cells"
302
+ :key="'hc-' + dayIndex + '-' + hourIndex"
303
+ :class="['hourly-heatmap-cell', 'level-' + cell.level]"
304
+ :title="cell.tooltip"
305
+ :aria-label="cell.tooltip">
306
+ </div>
307
+ </div>
308
+ <div class="hourly-heatmap-legend">
309
+ <span class="hourly-heatmap-legend-label">{{ t('usage.hourlyHeatmap.legend.less') }}</span>
310
+ <span class="hourly-heatmap-cell level-0"></span>
311
+ <span class="hourly-heatmap-cell level-1"></span>
312
+ <span class="hourly-heatmap-cell level-2"></span>
313
+ <span class="hourly-heatmap-cell level-3"></span>
314
+ <span class="hourly-heatmap-cell level-4"></span>
315
+ <span class="hourly-heatmap-legend-label">{{ t('usage.hourlyHeatmap.legend.more') }}</span>
316
+ </div>
317
+ </div>
318
+ </section>
319
+
310
320
  <section class="usage-card usage-card-top-paths">
311
321
  <div class="usage-card-title">{{ t('usage.paths.title') }}</div>
312
322
  <div v-if="!sessionUsageCharts.topPaths.length" class="usage-list-value">{{ t('usage.paths.empty') }}</div>
@@ -422,6 +422,9 @@ export async function loadActiveSessionDetail(api, options = {}) {
422
422
  if (res.sourceLabel) {
423
423
  this.activeSession.sourceLabel = res.sourceLabel;
424
424
  }
425
+ if (typeof res.derived === 'boolean') {
426
+ this.activeSession.derived = res.derived;
427
+ }
425
428
  if (res.sessionId) {
426
429
  this.activeSession.sessionId = res.sessionId;
427
430
  if (!this.activeSession.title) {
@@ -4,30 +4,34 @@
4
4
  设计系统 - Design Tokens
5
5
  ============================================ */
6
6
  :root {
7
- /* 色彩系统:温和中性暖灰 + 柔粉强调 */
7
+ /* 色彩系统:中性灰(更贴近 craft/shadcn 观感)+ 品牌强调 */
8
8
  --color-brand: #C77462;
9
9
  --color-brand-dark: #B45E4E;
10
- --color-brand-light: rgba(199, 116, 98, 0.14);
11
- --color-brand-subtle: rgba(199, 116, 98, 0.2);
10
+ --color-brand-light: rgba(199, 116, 98, 0.12);
11
+ --color-brand-subtle: rgba(199, 116, 98, 0.18);
12
12
 
13
- --color-bg: #F7F3EF;
14
- --color-surface: #FFFDFC;
15
- --color-surface-alt: #F8F3EE;
13
+ --color-bg: #FAFAFA;
14
+ --color-surface: #FFFFFF;
15
+ --color-surface-alt: #F4F4F5;
16
16
  --color-surface-elevated: #FFFFFF;
17
- --color-surface-tint: rgba(255, 253, 252, 0.9);
18
- --color-text-primary: #241F1C;
19
- --color-text-secondary: #5A514B;
20
- --color-text-tertiary: #7A6E66;
21
- --color-text-muted: #A39286;
22
- --color-border: #E7DDD4;
23
- --color-border-soft: rgba(163, 146, 134, 0.22);
24
- --color-border-strong: rgba(163, 146, 134, 0.4);
17
+ --color-surface-tint: rgba(255, 255, 255, 0.92);
18
+ --color-text-primary: #18181B;
19
+ --color-text-secondary: #3F3F46;
20
+ --color-text-tertiary: #71717A;
21
+ --color-text-muted: #A1A1AA;
22
+ --color-border: #E4E4E7;
23
+ --color-border-soft: rgba(24, 24, 27, 0.12);
24
+ --color-border-strong: rgba(24, 24, 27, 0.24);
25
25
 
26
26
  --color-success: #4B8B6A;
27
27
  --color-error: #C44536;
28
28
 
29
29
  --bg-warm-gradient:
30
- linear-gradient(180deg, #F7F3EF 0%, #F7F3EF 100%);
30
+ linear-gradient(180deg, #FAFAFA 0%, #FAFAFA 100%);
31
+
32
+ --color-bg-topbar-strong: rgba(250, 250, 250, 0.96);
33
+ --color-bg-topbar-soft: rgba(250, 250, 250, 0.9);
34
+ --color-bg-topbar-clear: rgba(250, 250, 250, 0);
31
35
 
32
36
  /* 字体系统 */
33
37
  --font-family-body: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
@@ -63,21 +67,21 @@
63
67
  --radius-sm: 8px;
64
68
  --radius-md: 10px;
65
69
  --radius-lg: 12px;
66
- --radius-xl: 18px;
70
+ --radius-xl: 16px;
67
71
  --radius-full: 50px;
68
72
 
69
- /* 阴影系统 - 温和、轻量 */
70
- --shadow-subtle: 0 1px 2px rgba(36, 31, 28, 0.03);
71
- --shadow-card: 0 4px 12px rgba(36, 31, 28, 0.045);
72
- --shadow-card-hover: 0 8px 22px rgba(36, 31, 28, 0.07);
73
- --shadow-float: 0 12px 28px rgba(36, 31, 28, 0.1);
74
- --shadow-raised: 0 8px 18px rgba(36, 31, 28, 0.08);
73
+ /* 阴影系统 - 更偏中性与克制 */
74
+ --shadow-subtle: 0 1px 2px rgba(24, 24, 27, 0.04);
75
+ --shadow-card: 0 1px 3px rgba(24, 24, 27, 0.06);
76
+ --shadow-card-hover: 0 8px 26px rgba(24, 24, 27, 0.12);
77
+ --shadow-float: 0 14px 36px rgba(24, 24, 27, 0.16);
78
+ --shadow-raised: 0 8px 18px rgba(24, 24, 27, 0.12);
75
79
  --shadow-modal:
76
- 0 8px 24px rgba(36, 31, 28, 0.08),
77
- 0 24px 64px rgba(36, 31, 28, 0.05);
80
+ 0 10px 30px rgba(24, 24, 27, 0.14),
81
+ 0 28px 72px rgba(24, 24, 27, 0.12);
78
82
  --shadow-input-focus:
79
83
  0 0 0 3px var(--color-brand-light),
80
- 0 1px 3px rgba(31, 26, 23, 0.04);
84
+ 0 1px 2px rgba(24, 24, 27, 0.06);
81
85
 
82
86
  /* 动画 - 更细腻的曲线 */
83
87
  --transition-instant: 100ms;
@@ -185,7 +185,7 @@ body::after {
185
185
  content: "";
186
186
  position: absolute;
187
187
  inset: -12px -18px;
188
- background: radial-gradient(circle at 50% 70%, rgba(247, 243, 239, 0.92), rgba(247, 243, 239, 0));
188
+ background: radial-gradient(circle at 50% 70%, rgba(250, 250, 250, 0.92), rgba(250, 250, 250, 0));
189
189
  filter: blur(2px);
190
190
  pointer-events: none;
191
191
  }
@@ -6,8 +6,8 @@
6
6
  .main-tab-btn {
7
7
  flex: 1;
8
8
  text-align: center;
9
- border: 1px solid rgba(216, 201, 184, 0.55);
10
- background: rgba(255, 255, 255, 0.95);
9
+ border: 1px solid var(--color-border);
10
+ background: var(--color-surface-tint);
11
11
  border-radius: var(--radius-lg);
12
12
  padding: 12px 14px;
13
13
  cursor: pointer;
@@ -26,9 +26,9 @@
26
26
 
27
27
  .main-tab-btn.active {
28
28
  border-color: var(--color-brand);
29
- box-shadow: 0 10px 24px rgba(27, 23, 20, 0.08);
29
+ box-shadow: var(--shadow-card-hover);
30
30
  color: var(--color-text-primary);
31
- background: linear-gradient(135deg, rgba(201, 94, 75, 0.12), rgba(255, 255, 255, 0.95));
31
+ background: linear-gradient(135deg, var(--color-brand-light), var(--color-surface-tint));
32
32
  }
33
33
 
34
34
  .status-strip {
@@ -52,7 +52,7 @@
52
52
 
53
53
  .lang-toggle-btn {
54
54
  border: 1px solid var(--color-border);
55
- background: rgba(255, 255, 255, 0.75);
55
+ background: var(--color-surface-tint);
56
56
  border-radius: 10px;
57
57
  padding: 10px 12px;
58
58
  font-size: 12px;
@@ -63,7 +63,7 @@
63
63
 
64
64
  .lang-toggle-btn:hover {
65
65
  border-color: rgba(0, 0, 0, 0.22);
66
- background: rgba(255, 255, 255, 0.95);
66
+ background: var(--color-surface);
67
67
  }
68
68
 
69
69
  .lang-toggle-btn.active {
@@ -77,7 +77,7 @@
77
77
  max-width: 100%;
78
78
  padding: 6px 10px;
79
79
  border: 1px solid var(--color-border);
80
- background: rgba(255, 253, 252, 0.82);
80
+ background: var(--color-surface-tint);
81
81
  box-shadow: none;
82
82
  display: inline-flex;
83
83
  flex-direction: row;
@@ -111,7 +111,7 @@
111
111
  padding: 10px 12px;
112
112
  border-radius: 10px;
113
113
  border: 1px solid var(--color-border);
114
- background: rgba(255, 253, 252, 0.88);
114
+ background: var(--color-surface-tint);
115
115
  box-shadow: none;
116
116
  display: grid;
117
117
  gap: 6px;
@@ -183,7 +183,7 @@
183
183
  z-index: 12;
184
184
  margin: 0 -28px 14px;
185
185
  padding: 0 28px 12px;
186
- background: linear-gradient(180deg, rgba(247, 243, 239, 0.96) 0%, rgba(247, 243, 239, 0.92) 78%, rgba(247, 243, 239, 0) 100%);
186
+ background: linear-gradient(180deg, var(--color-bg-topbar-strong) 0%, var(--color-bg-topbar-soft) 78%, var(--color-bg-topbar-clear) 100%);
187
187
  backdrop-filter: blur(8px);
188
188
  }
189
189
 
@@ -381,6 +381,23 @@
381
381
  white-space: nowrap;
382
382
  }
383
383
 
384
+ .session-item-hot {
385
+ font-size: var(--font-size-caption);
386
+ padding: 2px 6px;
387
+ border-radius: 999px;
388
+ background: rgba(208, 88, 58, 0.14);
389
+ border: 1px solid rgba(208, 88, 58, 0.26);
390
+ color: rgba(140, 46, 24, 0.9);
391
+ line-height: 1;
392
+ white-space: nowrap;
393
+ }
394
+
395
+ .session-item-cwd {
396
+ flex: 1 1 160px;
397
+ min-width: 160px;
398
+ color: var(--color-text-muted);
399
+ }
400
+
384
401
  .session-preview {
385
402
  border: 1px solid var(--color-border-soft);
386
403
  border-radius: var(--radius-xl);
@@ -37,6 +37,68 @@
37
37
  border-top: 1px dashed var(--color-border-soft);
38
38
  }
39
39
 
40
+ .session-filter-chips {
41
+ display: flex;
42
+ flex-wrap: wrap;
43
+ gap: 8px;
44
+ margin-top: 4px;
45
+ margin-bottom: 2px;
46
+ }
47
+
48
+ .session-filter-chip {
49
+ display: inline-flex;
50
+ align-items: center;
51
+ gap: 6px;
52
+ max-width: 100%;
53
+ padding: 6px 10px;
54
+ border-radius: 999px;
55
+ border: 1px solid var(--color-border-soft);
56
+ background: linear-gradient(to bottom, rgba(255, 255, 255, 0.92) 0%, rgba(255, 255, 255, 0.78) 100%);
57
+ color: var(--color-text-secondary);
58
+ font-size: var(--font-size-caption);
59
+ font-weight: var(--font-weight-secondary);
60
+ cursor: pointer;
61
+ transition: all var(--transition-fast) var(--ease-spring);
62
+ box-shadow: inset 0 1px 2px rgba(31, 26, 23, 0.04);
63
+ min-width: 0;
64
+ }
65
+
66
+ .session-filter-chip:hover {
67
+ border-color: var(--color-border-strong);
68
+ transform: translateY(-1px);
69
+ }
70
+
71
+ .session-filter-chip-title {
72
+ opacity: 0.75;
73
+ flex: 0 0 auto;
74
+ }
75
+
76
+ .session-filter-chip-value {
77
+ min-width: 0;
78
+ overflow: hidden;
79
+ text-overflow: ellipsis;
80
+ white-space: nowrap;
81
+ max-width: 520px;
82
+ }
83
+
84
+ .session-filter-chip-close {
85
+ display: inline-flex;
86
+ align-items: center;
87
+ justify-content: center;
88
+ width: 18px;
89
+ height: 18px;
90
+ border-radius: 999px;
91
+ border: 1px solid rgba(70, 86, 110, 0.22);
92
+ background: rgba(70, 86, 110, 0.08);
93
+ line-height: 1;
94
+ flex: 0 0 auto;
95
+ }
96
+
97
+ .session-filter-chip:focus-visible {
98
+ outline: 3px solid rgba(201, 94, 75, 0.25);
99
+ outline-offset: 2px;
100
+ }
101
+
40
102
  .session-toolbar-footer .quick-option {
41
103
  margin: 0;
42
104
  padding: 6px 10px;