codexmate 0.0.54 → 0.0.56

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 (42) hide show
  1. package/README.md +3 -0
  2. package/README.vi.md +172 -0
  3. package/README.zh.md +2 -0
  4. package/cli/local-bridge.js +221 -22
  5. package/cli.js +666 -6
  6. package/package.json +1 -1
  7. package/web-ui/app.js +40 -0
  8. package/web-ui/logic.claude.mjs +8 -0
  9. package/web-ui/modules/app.computed.session.mjs +210 -0
  10. package/web-ui/modules/app.methods.claude-config.mjs +196 -13
  11. package/web-ui/modules/app.methods.codex-config.mjs +294 -65
  12. package/web-ui/modules/app.methods.index.mjs +4 -0
  13. package/web-ui/modules/app.methods.navigation.mjs +7 -1
  14. package/web-ui/modules/app.methods.provider-cache.mjs +223 -0
  15. package/web-ui/modules/app.methods.providers.mjs +15 -1
  16. package/web-ui/modules/app.methods.runtime.mjs +7 -2
  17. package/web-ui/modules/app.methods.session-actions.mjs +36 -0
  18. package/web-ui/modules/app.methods.session-browser.mjs +3 -0
  19. package/web-ui/modules/app.methods.session-trash.mjs +3 -0
  20. package/web-ui/modules/app.methods.startup-claude.mjs +47 -4
  21. package/web-ui/modules/app.methods.web-ui-preferences.mjs +161 -0
  22. package/web-ui/modules/i18n/locales/en.mjs +106 -3
  23. package/web-ui/modules/i18n/locales/ja.mjs +103 -3
  24. package/web-ui/modules/i18n/locales/vi.mjs +950 -0
  25. package/web-ui/modules/i18n/locales/zh-tw.mjs +103 -3
  26. package/web-ui/modules/i18n/locales/zh.mjs +103 -3
  27. package/web-ui/modules/provider-default-names.mjs +25 -0
  28. package/web-ui/partials/index/layout-header.html +18 -5
  29. package/web-ui/partials/index/modal-health-check.html +69 -5
  30. package/web-ui/partials/index/modals-basic.html +182 -0
  31. package/web-ui/partials/index/panel-config-codex.html +2 -2
  32. package/web-ui/partials/index/panel-sessions.html +99 -19
  33. package/web-ui/partials/index/panel-settings.html +20 -0
  34. package/web-ui/res/web-ui-render.precompiled.js +742 -113
  35. package/web-ui/session-helpers.mjs +4 -1
  36. package/web-ui/styles/layout-shell.css +51 -1
  37. package/web-ui/styles/responsive.css +98 -0
  38. package/web-ui/styles/sessions-preview.css +212 -2
  39. package/web-ui/styles/sessions-toolbar-trash.css +61 -18
  40. package/web-ui/styles/settings-panel.css +398 -0
  41. package/web-ui/styles/skills-list.css +122 -0
  42. package/web-ui/styles/titles-cards.css +52 -0
@@ -134,6 +134,188 @@
134
134
  </div>
135
135
  </div>
136
136
 
137
+ <!-- 项目功能公告模态框 -->
138
+ <div v-if="showProviderCacheAnnouncementModal" class="modal-overlay project-announcement-overlay" @click.self="closeProviderCacheAnnouncementModal">
139
+ <div class="modal provider-cache-announcement-modal project-announcement-modal" role="dialog" aria-modal="true" aria-labelledby="provider-cache-announcement-title">
140
+ <button type="button" class="project-announcement-close" :aria-label="t('announcement.project.closeAria')" @click="closeProviderCacheAnnouncementModal">×</button>
141
+ <div class="project-announcement-header">
142
+ <div class="project-announcement-eyebrow">{{ t('announcement.project.eyebrow') }}</div>
143
+ <div class="modal-title project-announcement-title" id="provider-cache-announcement-title">{{ t('announcement.project.title') }}</div>
144
+ <p class="provider-cache-announcement-copy provider-cache-announcement-lede">{{ t('announcement.project.subtitle') }}</p>
145
+ </div>
146
+
147
+ <div class="project-announcement-feature-grid" :aria-label="t('announcement.project.features.aria')">
148
+ <article class="project-announcement-feature-card">
149
+ <div class="project-announcement-feature-title">{{ t('announcement.project.feature.config.title') }}</div>
150
+ <div class="project-announcement-feature-meta">{{ t('announcement.project.feature.config.meta') }}</div>
151
+ </article>
152
+ <article class="project-announcement-feature-card">
153
+ <div class="project-announcement-feature-title">{{ t('announcement.project.feature.sessions.title') }}</div>
154
+ <div class="project-announcement-feature-meta">{{ t('announcement.project.feature.sessions.meta') }}</div>
155
+ </article>
156
+ <article class="project-announcement-feature-card">
157
+ <div class="project-announcement-feature-title">{{ t('announcement.project.feature.usage.title') }}</div>
158
+ <div class="project-announcement-feature-meta">{{ t('announcement.project.feature.usage.meta') }}</div>
159
+ </article>
160
+ <article class="project-announcement-feature-card">
161
+ <div class="project-announcement-feature-title">{{ t('announcement.project.feature.tasks.title') }}</div>
162
+ <div class="project-announcement-feature-meta">{{ t('announcement.project.feature.tasks.meta') }}</div>
163
+ </article>
164
+ <article class="project-announcement-feature-card">
165
+ <div class="project-announcement-feature-title">{{ t('announcement.project.feature.skills.title') }}</div>
166
+ <div class="project-announcement-feature-meta">{{ t('announcement.project.feature.skills.meta') }}</div>
167
+ </article>
168
+ <article class="project-announcement-feature-card">
169
+ <div class="project-announcement-feature-title">{{ t('announcement.project.feature.data.title') }}</div>
170
+ <div class="project-announcement-feature-meta">{{ t('announcement.project.feature.data.meta') }}</div>
171
+ </article>
172
+ </div>
173
+
174
+ <details class="project-announcement-cache-section" aria-labelledby="project-announcement-cache-title">
175
+ <summary class="project-announcement-section-head">
176
+ <span>
177
+ <span class="project-announcement-section-title" id="project-announcement-cache-title">{{ t('announcement.project.cache.title') }}</span>
178
+ <span class="project-announcement-section-meta">{{ t('announcement.project.cache.meta') }}</span>
179
+ </span>
180
+ <span v-if="getProviderCacheAnnouncementSummary().loadedAt" class="provider-cache-loaded-at">{{ t('modal.providerCache.loadedAt') }}: {{ getProviderCacheAnnouncementSummary().loadedAt }}</span>
181
+ </summary>
182
+
183
+ <div v-if="providerCacheError" class="state-message error provider-cache-error">
184
+ {{ providerCacheError }}
185
+ </div>
186
+ <div v-if="providerCacheSyncMessage" class="state-message provider-cache-sync-message">
187
+ {{ providerCacheSyncMessage }}
188
+ </div>
189
+ <div v-if="providerCacheLoading && !providerCacheLoadedOnce" class="state-message">
190
+ {{ t('modal.providerCache.loading') }}
191
+ </div>
192
+ <div class="provider-cache-announcement-body">
193
+ <div class="project-announcement-mini-status" :aria-label="t('announcement.project.status.aria')">
194
+ <span>{{ t('announcement.project.status.provider') }}: <strong>{{ currentProvider || t('common.notSelected') }}</strong></span>
195
+ <span>{{ t('announcement.project.status.model') }}: <strong>{{ currentModel || t('common.notSelected') }}</strong></span>
196
+ <span>{{ t('announcement.project.status.cacheFiles') }}: <strong>{{ getProviderCacheAnnouncementSummary().fileCount }}</strong></span>
197
+ </div>
198
+ <div class="provider-cache-summary-grid">
199
+ <div class="provider-cache-summary-card">
200
+ <span class="provider-cache-summary-label">{{ t('announcement.project.cache.files') }}</span>
201
+ <strong>{{ getProviderCacheAnnouncementSummary().fileCount }}</strong>
202
+ </div>
203
+ <div class="provider-cache-summary-card">
204
+ <span class="provider-cache-summary-label">{{ t('announcement.project.cache.providers') }}</span>
205
+ <strong>{{ getProviderCacheAnnouncementSummary().providerCount }}</strong>
206
+ </div>
207
+ <div class="provider-cache-summary-card">
208
+ <span class="provider-cache-summary-label">{{ t('announcement.project.cache.groups') }}</span>
209
+ <strong>{{ getProviderCacheAnnouncementSummary().groupCount }}</strong>
210
+ </div>
211
+ </div>
212
+ <div class="provider-cache-announcement-list" :aria-label="t('announcement.project.cache.groupList')">
213
+ <div v-for="group in getProviderCacheAnnouncementGroups()" :key="group.key" class="provider-cache-announcement-row">
214
+ <span>{{ group.label }}</span>
215
+ <span>{{ t('announcement.project.cache.groupSummary', { files: group.existingCount, providers: group.providerCount }) }}</span>
216
+ </div>
217
+ </div>
218
+ <div class="project-announcement-cache-actions">
219
+ <button type="button" class="btn-tool btn-tool-compact" @click="syncProviderCacheRecords" :disabled="providerCacheLoading || providerCacheSyncing">
220
+ {{ providerCacheSyncing ? t('modal.providerCache.syncing') : t('announcement.project.cache.sync') }}
221
+ </button>
222
+ <button type="button" class="btn-tool btn-tool-compact" @click="loadProviderCacheRecords" :disabled="providerCacheLoading">
223
+ {{ providerCacheLoading ? t('modal.providerCache.refreshing') : t('announcement.project.cache.refresh') }}
224
+ </button>
225
+ <button type="button" class="btn-tool btn-tool-compact" @click="openProviderCacheDetailsFromAnnouncement">
226
+ {{ t('announcement.project.cache.details') }}
227
+ </button>
228
+ </div>
229
+ </div>
230
+ </details>
231
+
232
+ <div class="btn-group provider-cache-footer project-announcement-footer">
233
+ <button class="btn btn-confirm" @click="closeProviderCacheAnnouncementModal">{{ t('announcement.project.primaryAction') }}</button>
234
+ </div>
235
+ </div>
236
+ </div>
237
+
238
+ <!-- Provider 缓存记录模态框 -->
239
+ <div v-if="showProviderCacheModal" class="modal-overlay" @click.self="closeProviderCacheModal">
240
+ <div class="modal modal-wide provider-cache-modal" role="dialog" aria-modal="true" aria-labelledby="provider-cache-modal-title">
241
+ <div class="modal-title" id="provider-cache-modal-title">{{ t('modal.providerCache.title') }}</div>
242
+ <div class="form-hint provider-cache-root">
243
+ {{ t('modal.providerCache.root') }}: {{ (providerCacheRecords && providerCacheRecords.root) || '~/.codexmate' }}
244
+ </div>
245
+
246
+ <div class="provider-cache-toolbar">
247
+ <button type="button" class="btn-tool btn-tool-compact" @click="loadProviderCacheRecords" :disabled="providerCacheLoading">
248
+ {{ providerCacheLoading ? t('modal.providerCache.refreshing') : t('modal.providerCache.refresh') }}
249
+ </button>
250
+ <button type="button" class="btn-tool btn-tool-compact" @click="syncProviderCacheRecords" :disabled="providerCacheLoading || providerCacheSyncing">
251
+ {{ providerCacheSyncing ? t('modal.providerCache.syncing') : t('modal.providerCache.sync') }}
252
+ </button>
253
+ <span v-if="providerCacheLoadedAt" class="provider-cache-loaded-at">{{ t('modal.providerCache.loadedAt') }}: {{ providerCacheLoadedAt }}</span>
254
+ </div>
255
+
256
+ <div v-if="providerCacheError" class="state-message error provider-cache-error">
257
+ {{ providerCacheError }}
258
+ </div>
259
+ <div v-if="providerCacheSyncMessage" class="state-message provider-cache-sync-message">
260
+ {{ providerCacheSyncMessage }}
261
+ </div>
262
+ <div v-if="providerCacheLoading && !providerCacheLoadedOnce" class="state-message">
263
+ {{ t('modal.providerCache.loading') }}
264
+ </div>
265
+ <div v-else class="provider-cache-body">
266
+ <div class="provider-cache-groups">
267
+ <section v-for="group in getProviderCacheGroups()" :key="group.key" class="provider-cache-group">
268
+ <div class="provider-cache-group-header">
269
+ <div class="provider-cache-group-title">{{ group.label }}</div>
270
+ <div class="provider-cache-group-meta">{{ t('modal.providerCache.groupMeta', { count: group.existingCount || 0 }) }}</div>
271
+ </div>
272
+ <div v-if="!hasProviderCacheExistingFiles(group)" class="provider-cache-empty">
273
+ {{ t('modal.providerCache.empty') }}
274
+ </div>
275
+ <div v-else class="provider-cache-file-list">
276
+ <article v-for="file in getProviderCacheExistingFiles(group)" :key="getProviderCacheFileKey(file)" class="provider-cache-file">
277
+ <div class="provider-cache-file-header">
278
+ <div>
279
+ <div class="provider-cache-file-name">{{ file.name }}</div>
280
+ <div class="provider-cache-file-summary">{{ getProviderCacheFileSummary(file) }}</div>
281
+ </div>
282
+ <div class="provider-cache-file-meta">
283
+ <span>{{ formatProviderCacheFileSize(file.size) }}</span>
284
+ <span v-if="file.mtime">{{ file.mtime }}</span>
285
+ </div>
286
+ </div>
287
+ <div class="provider-cache-file-path">{{ getProviderCacheFilePath(file) }}</div>
288
+
289
+ <div v-if="hasProviderCacheProviders(file)" class="provider-cache-provider-list">
290
+ <details v-for="(provider, providerIndex) in getProviderCacheFileProviders(file)" :key="provider.name || (getProviderCacheFileKey(file) + ':' + providerIndex)" class="provider-cache-provider" open>
291
+ <summary class="provider-cache-provider-summary">
292
+ <span class="provider-cache-provider-name">{{ provider.name || 'provider' }}</span>
293
+ <span class="provider-cache-provider-badges">
294
+ <span v-for="item in getProviderCacheProviderMeta(provider)" :key="item.label" class="provider-cache-provider-badge">
295
+ {{ item.label }}: {{ item.value }}
296
+ </span>
297
+ </span>
298
+ </summary>
299
+ <pre class="provider-cache-json provider-cache-json-compact">{{ getProviderCacheProviderText(provider) }}</pre>
300
+ </details>
301
+ </div>
302
+
303
+ <details class="provider-cache-raw" :open="!hasProviderCacheProviders(file) || file.ok === false">
304
+ <summary>{{ file.ok === false ? t('modal.providerCache.errorDetails') : t('modal.providerCache.rawJson') }}</summary>
305
+ <pre :class="['provider-cache-json', { error: file.ok === false }]">{{ getProviderCacheRecordText(file) }}</pre>
306
+ </details>
307
+ </article>
308
+ </div>
309
+ </section>
310
+ </div>
311
+ </div>
312
+
313
+ <div class="btn-group provider-cache-footer">
314
+ <button class="btn btn-confirm" @click="closeProviderCacheModal">{{ t('common.close') }}</button>
315
+ </div>
316
+ </div>
317
+ </div>
318
+
137
319
  <!-- 添加Claude配置模态框 -->
138
320
  <div v-if="showClaudeConfigModal" class="modal-overlay" @click.self="closeClaudeConfigModal">
139
321
  <div class="modal" role="dialog" aria-modal="true" aria-labelledby="add-claude-config-modal-title">
@@ -47,7 +47,7 @@
47
47
 
48
48
  <div class="tool-config-write-scope" :class="{ locked: !isToolConfigWriteAllowed('codex') }">
49
49
  <div class="tool-config-write-body">
50
- <button class="btn-add" @click="showAddProviderKey = false; showAddModal = true" v-if="!loading && !initError" :disabled="!isToolConfigWriteAllowed('codex')">
50
+ <button class="btn-add" @click="openAddProviderModal" v-if="!loading && !initError" :disabled="!isToolConfigWriteAllowed('codex')">
51
51
  <svg class="icon" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2"><path d="M10 4v12M4 10h12"/></svg>
52
52
  {{ t('config.addProvider') }}
53
53
  </button>
@@ -131,7 +131,7 @@
131
131
  <div class="selector-section">
132
132
  <div class="selector-header"><span class="selector-title">{{ t('config.health.title') }}</span></div>
133
133
  <button class="btn-tool" @click="runHealthCheck" :disabled="healthCheckLoading || loading || !!initError">{{ healthCheckLoading ? t('config.health.running') : t('config.health.run') }}</button>
134
- <div class="config-template-hint">{{ t('config.health.hint') }}</div>
134
+ <div class="config-template-hint">{{ t('config.health.codexHint') }}</div>
135
135
  </div>
136
136
  </template>
137
137
 
@@ -21,28 +21,29 @@
21
21
  </div>
22
22
  </div>
23
23
  <div v-else>
24
- <div class="selector-section">
24
+ <div class="selector-section session-selector-section">
25
25
  <div class="selector-header" style="display:none">
26
26
  <span class="selector-title">{{ t('sessions.sourceTitle') }}</span>
27
27
  <div class="selector-actions sessions-header-actions"></div>
28
28
  </div>
29
+ <div class="session-source-tabs-row">
30
+ <div class="session-source-pills" role="group" :aria-label="t('sessions.sourceTitle')">
31
+ <button
32
+ v-for="src in sessionSourceOptions"
33
+ :key="src.value"
34
+ :class="['session-source-pill', { active: sessionFilterSource === src.value }]"
35
+ :data-source="src.value"
36
+ @click="setSessionSource(src.value)"
37
+ :disabled="sessionsLoading"
38
+ :aria-pressed="sessionFilterSource === src.value"
39
+ type="button">
40
+ <span class="session-source-pill-dot"></span>
41
+ <span class="session-source-pill-label">{{ src.label }}</span>
42
+ </button>
43
+ </div>
44
+ </div>
29
45
  <div class="session-toolbar">
30
46
  <div class="session-toolbar-group session-toolbar-primary">
31
- <div class="session-source-pills" role="radiogroup" aria-label="Session source">
32
- <button
33
- v-for="src in sessionSourceOptions"
34
- :key="src.value"
35
- :class="['session-source-pill', { active: sessionFilterSource === src.value }]"
36
- :data-source="src.value"
37
- @click="setSessionSource(src.value)"
38
- :disabled="sessionsLoading"
39
- :aria-pressed="sessionFilterSource === src.value"
40
- role="radio"
41
- type="button">
42
- <span class="session-source-pill-dot"></span>
43
- <span class="session-source-pill-label">{{ src.label }}</span>
44
- </button>
45
- </div>
46
47
  <select
47
48
  class="session-path-select"
48
49
  v-model="sessionPathFilter"
@@ -119,11 +120,11 @@
119
120
  </div>
120
121
  </div>
121
122
 
122
- <div v-if="sessionsLoading" class="state-message">
123
+ <div v-if="sessionsLoading && sessionsList.length === 0" class="state-message">
123
124
  {{ t('sessions.loadingList') }}
124
125
  </div>
125
126
 
126
- <div v-else-if="sessionsList.length === 0" class="session-empty">
127
+ <div v-else-if="!sessionsLoading && sessionsList.length === 0" class="session-empty">
127
128
  {{ t('sessions.empty') }}
128
129
  </div>
129
130
 
@@ -241,6 +242,14 @@
241
242
  :aria-label="(activeSession && sessionExporting[getSessionExportKey(activeSession)]) ? t('sessions.preview.exporting') : t('sessions.preview.export')">
242
243
  <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M8 2v8"/><polyline points="4 7 8 10.5 12 7"/><path d="M2.5 12v1.5a1 1 0 0 0 1 1h9a1 1 0 0 0 1-1V12"/></svg>
243
244
  </button>
245
+ <button
246
+ class="btn-session-open session-brief-copy"
247
+ @click="copySessionWorkspaceBrief"
248
+ :disabled="!activeSessionWorkspaceSummary.available"
249
+ :title="t('sessions.workspace.copy')"
250
+ :aria-label="t('sessions.workspace.copy')">
251
+ <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M4 2.5h6.5L13 5v8.5H4z"/><path d="M10.5 2.5V5H13"/><path d="M6 8h5"/><path d="M6 10.5h5"/></svg>
252
+ </button>
244
253
  <div class="session-link-group">
245
254
  <button
246
255
  class="btn-session-open"
@@ -294,12 +303,77 @@
294
303
  </div>
295
304
 
296
305
  <div v-else class="session-preview-body">
306
+ <section v-if="activeSessionWorkspaceSummary.available" class="session-workspace" :aria-label="t('sessions.workspace.title')">
307
+ <div class="session-workspace-header">
308
+ <div>
309
+ <div class="session-workspace-kicker">{{ t('sessions.workspace.kicker') }}</div>
310
+ <h3 class="session-workspace-title">{{ t('sessions.workspace.title') }}</h3>
311
+ <p class="session-workspace-subtitle">
312
+ {{ t('sessions.workspace.subtitle', { count: activeSessionWorkspaceSummary.messageCount }) }}
313
+ </p>
314
+ </div>
315
+ <button class="btn-mini" type="button" @click="copySessionWorkspaceBrief">
316
+ {{ t('sessions.workspace.copy') }}
317
+ </button>
318
+ </div>
319
+
320
+ <div class="session-workspace-metrics">
321
+ <div v-for="metric in activeSessionWorkspaceSummary.metrics" :key="metric.key" class="session-workspace-metric">
322
+ <span class="session-workspace-metric-value">{{ metric.value }}</span>
323
+ <span class="session-workspace-metric-label">{{ metric.label }}</span>
324
+ </div>
325
+ </div>
326
+
327
+ <div class="session-workspace-grid">
328
+ <div class="session-workspace-card session-workspace-card-wide">
329
+ <div class="session-workspace-card-title">{{ t('sessions.workspace.signals') }}</div>
330
+ <ul v-if="activeSessionWorkspaceSummary.signals.length" class="session-workspace-list">
331
+ <li v-for="(item, index) in activeSessionWorkspaceSummary.signals" :key="'signal-' + index">{{ item }}</li>
332
+ </ul>
333
+ <div v-else class="session-workspace-empty">{{ t('sessions.workspace.empty') }}</div>
334
+ </div>
335
+
336
+ <div class="session-workspace-card">
337
+ <div class="session-workspace-card-title">{{ t('sessions.workspace.commands') }}</div>
338
+ <ul v-if="activeSessionWorkspaceSummary.commands.length" class="session-workspace-list session-workspace-code-list">
339
+ <li v-for="(item, index) in activeSessionWorkspaceSummary.commands" :key="'command-' + index"><code>{{ item }}</code></li>
340
+ </ul>
341
+ <div v-else class="session-workspace-empty">{{ t('sessions.workspace.noneCommands') }}</div>
342
+ </div>
343
+
344
+ <div class="session-workspace-card">
345
+ <div class="session-workspace-card-title">{{ t('sessions.workspace.files') }}</div>
346
+ <ul v-if="activeSessionWorkspaceSummary.files.length" class="session-workspace-list session-workspace-code-list">
347
+ <li v-for="(item, index) in activeSessionWorkspaceSummary.files" :key="'file-' + index"><code>{{ item }}</code></li>
348
+ </ul>
349
+ <div v-else class="session-workspace-empty">{{ t('sessions.workspace.noneFiles') }}</div>
350
+ </div>
351
+
352
+ <div class="session-workspace-card">
353
+ <div class="session-workspace-card-title">{{ t('sessions.workspace.links') }}</div>
354
+ <ul v-if="activeSessionWorkspaceSummary.links.length" class="session-workspace-list">
355
+ <li v-for="(item, index) in activeSessionWorkspaceSummary.links" :key="'link-' + index">{{ item }}</li>
356
+ </ul>
357
+ <div v-else class="session-workspace-empty">{{ t('sessions.workspace.noneLinks') }}</div>
358
+ </div>
359
+
360
+ <div class="session-workspace-card" :class="{ 'session-workspace-card-alert': activeSessionWorkspaceSummary.risks.length || activeSessionWorkspaceSummary.nextSteps.length }">
361
+ <div class="session-workspace-card-title">{{ t('sessions.workspace.risks') }}</div>
362
+ <ul v-if="activeSessionWorkspaceSummary.risks.length || activeSessionWorkspaceSummary.nextSteps.length" class="session-workspace-list">
363
+ <li v-for="(item, index) in activeSessionWorkspaceSummary.risks" :key="'risk-' + index">{{ item }}</li>
364
+ <li v-for="(item, index) in activeSessionWorkspaceSummary.nextSteps" :key="'next-' + index">{{ item }}</li>
365
+ </ul>
366
+ <div v-else class="session-workspace-empty">{{ t('sessions.workspace.noneRisks') }}</div>
367
+ </div>
368
+ </div>
369
+ </section>
370
+
297
371
  <div class="session-preview-messages">
298
372
  <div v-if="activeSessionDetailClipped" class="session-item-sub session-item-wrap">
299
373
  {{ t('sessions.preview.clipped', { count: activeSessionMessages.length }) }}
300
374
  </div>
301
375
  <div
302
- v-for="(msg, idx) in activeSessionMessages"
376
+ v-for="(msg, idx) in activeSessionVisibleMessages"
303
377
  :key="getRecordRenderKey(msg, idx)"
304
378
  v-memo="[msg.text, msg.timestamp, msg.roleLabel, msg.normalizedRole]"
305
379
  :data-message-key="getRecordRenderKey(msg, idx)"
@@ -313,6 +387,12 @@
313
387
  </div>
314
388
  <div class="session-msg-content" v-html="highlightQueryText(msg.text) || ''"></div>
315
389
  </div>
390
+ <div v-if="canLoadMoreSessionMessages" class="session-preview-load-more">
391
+ <span>{{ t('sessions.preview.shownCount', { shown: activeSessionVisibleMessages.length, total: activeSessionMessages.length }) }}</span>
392
+ <button class="btn-mini" type="button" @click="loadMoreSessionMessages" :disabled="sessionPreviewLoadingMore">
393
+ {{ sessionPreviewLoadingMore ? t('sessions.preview.loadingMore') : t('sessions.preview.loadMore', { remain: sessionPreviewRemainingCount }) }}
394
+ </button>
395
+ </div>
316
396
  </div>
317
397
  </div>
318
398
  </div>
@@ -166,6 +166,26 @@
166
166
  <input ref="codexImportInput" class="sr-only" type="file" accept=".zip" @change="handleCodexImportChange">
167
167
  </section>
168
168
 
169
+ <section class="settings-card" :aria-label="t('settings.providerCache.title')">
170
+ <div class="settings-card-main">
171
+ <div class="settings-card-content">
172
+ <div class="settings-card-title">{{ t('settings.providerCache.title') }}</div>
173
+ <p class="settings-card-desc">{{ t('settings.providerCache.meta') }}</p>
174
+ <p class="settings-card-hint">{{ t('settings.providerCache.hint') }}</p>
175
+ <p v-if="providerCacheSyncMessage" class="settings-card-hint provider-cache-sync-message">{{ providerCacheSyncMessage }}</p>
176
+ <p v-if="providerCacheError" class="settings-card-hint form-error">{{ providerCacheError }}</p>
177
+ </div>
178
+ </div>
179
+ <div class="settings-card-actions">
180
+ <button class="settings-card-action" @click="openProviderCacheModal({ forceRefresh: true })" :disabled="providerCacheLoading || providerCacheSyncing">
181
+ {{ providerCacheLoading ? t('settings.providerCache.loading') : t('settings.providerCache.open') }}
182
+ </button>
183
+ <button class="settings-card-action" @click="syncProviderCacheRecords" :disabled="providerCacheLoading || providerCacheSyncing">
184
+ {{ providerCacheSyncing ? t('settings.providerCache.syncing') : t('settings.providerCache.sync') }}
185
+ </button>
186
+ </div>
187
+ </section>
188
+
169
189
  <section class="settings-card" :aria-label="t('settings.trashConfig.title')">
170
190
  <div class="settings-card-main">
171
191
  <div class="settings-card-content">