codexmate 0.0.27 → 0.0.29
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.
- package/README.md +1 -1
- package/README.zh.md +1 -1
- package/cli/builtin-proxy.js +430 -4
- package/cli/openai-bridge.js +498 -13
- package/cli.js +130 -41
- package/lib/cli-models-utils.js +71 -10
- package/lib/cli-webhook.js +126 -0
- package/package.json +76 -74
- package/plugins/prompt-templates/computed.mjs +1 -1
- package/plugins/prompt-templates/methods.mjs +0 -66
- package/plugins/prompt-templates/overview.mjs +1 -0
- package/web-ui/app.js +21 -16
- package/web-ui/index.html +1 -0
- package/web-ui/logic.codex.mjs +69 -0
- package/web-ui/modules/app.computed.dashboard.mjs +54 -0
- package/web-ui/modules/app.computed.session.mjs +22 -17
- package/web-ui/modules/app.methods.claude-config.mjs +24 -8
- package/web-ui/modules/app.methods.codex-config.mjs +35 -3
- package/web-ui/modules/app.methods.index.mjs +2 -0
- package/web-ui/modules/app.methods.navigation.mjs +21 -3
- package/web-ui/modules/app.methods.providers.mjs +96 -7
- package/web-ui/modules/app.methods.session-actions.mjs +3 -6
- package/web-ui/modules/app.methods.session-browser.mjs +1 -6
- package/web-ui/modules/app.methods.session-trash.mjs +6 -7
- package/web-ui/modules/app.methods.startup-claude.mjs +8 -1
- package/web-ui/modules/app.methods.webhook.mjs +79 -0
- package/web-ui/modules/i18n.dict.mjs +1104 -104
- package/web-ui/modules/i18n.mjs +9 -3
- package/web-ui/modules/provider-url-display.mjs +17 -0
- package/web-ui/partials/index/layout-header.html +25 -0
- package/web-ui/partials/index/modals-basic.html +0 -3
- package/web-ui/partials/index/panel-config-claude.html +10 -3
- package/web-ui/partials/index/panel-config-codex.html +44 -4
- package/web-ui/partials/index/panel-plugins.html +3 -29
- package/web-ui/partials/index/panel-sessions.html +0 -10
- package/web-ui/partials/index/panel-settings.html +93 -177
- package/web-ui/partials/index/panel-trash.html +88 -0
- package/web-ui/session-helpers.mjs +2 -2
- package/web-ui/styles/base-theme.css +47 -34
- package/web-ui/styles/controls-forms.css +27 -28
- package/web-ui/styles/docs-panel.css +63 -39
- package/web-ui/styles/layout-shell.css +69 -46
- package/web-ui/styles/modals-core.css +12 -10
- package/web-ui/styles/navigation-panels.css +36 -35
- package/web-ui/styles/responsive.css +4 -4
- package/web-ui/styles/sessions-list.css +10 -6
- package/web-ui/styles/settings-panel.css +197 -33
- package/web-ui/styles/titles-cards.css +90 -26
- package/web-ui/styles/trash-panel.css +90 -0
- package/web-ui/styles/webhook.css +81 -0
- package/web-ui/styles.css +2 -0
package/web-ui/modules/i18n.mjs
CHANGED
|
@@ -4,7 +4,9 @@ const I18N_STORAGE_KEY = 'codexmateLang';
|
|
|
4
4
|
|
|
5
5
|
function normalizeLang(value) {
|
|
6
6
|
const normalized = typeof value === 'string' ? value.trim().toLowerCase() : '';
|
|
7
|
-
|
|
7
|
+
if (normalized === 'en') return 'en';
|
|
8
|
+
if (normalized === 'ja') return 'ja';
|
|
9
|
+
return 'zh';
|
|
8
10
|
}
|
|
9
11
|
|
|
10
12
|
function interpolate(template, params) {
|
|
@@ -26,7 +28,9 @@ export function createI18nMethods() {
|
|
|
26
28
|
this.lang = next;
|
|
27
29
|
try {
|
|
28
30
|
if (typeof document !== 'undefined' && document.documentElement) {
|
|
29
|
-
|
|
31
|
+
if (next === 'en') document.documentElement.lang = 'en';
|
|
32
|
+
else if (next === 'ja') document.documentElement.lang = 'ja';
|
|
33
|
+
else document.documentElement.lang = 'zh-CN';
|
|
30
34
|
}
|
|
31
35
|
} catch (_) {}
|
|
32
36
|
},
|
|
@@ -40,7 +44,9 @@ export function createI18nMethods() {
|
|
|
40
44
|
} catch (_) {}
|
|
41
45
|
try {
|
|
42
46
|
if (typeof document !== 'undefined' && document.documentElement) {
|
|
43
|
-
|
|
47
|
+
if (next === 'en') document.documentElement.lang = 'en';
|
|
48
|
+
else if (next === 'ja') document.documentElement.lang = 'ja';
|
|
49
|
+
else document.documentElement.lang = 'zh-CN';
|
|
44
50
|
}
|
|
45
51
|
} catch (_) {}
|
|
46
52
|
},
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export function getProviderDisplayUrl(provider) {
|
|
2
|
+
if (!provider) return '';
|
|
3
|
+
const bridge = typeof provider.codexmate_bridge === 'string' ? provider.codexmate_bridge.trim() : '';
|
|
4
|
+
if (bridge === 'openai') {
|
|
5
|
+
const upstream = typeof provider.upstreamUrl === 'string' ? provider.upstreamUrl.trim() : '';
|
|
6
|
+
if (upstream) return upstream;
|
|
7
|
+
}
|
|
8
|
+
return provider.url || '';
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function checkIsTransformProvider(provider) {
|
|
12
|
+
if (!provider || typeof provider !== 'object') return false;
|
|
13
|
+
const bridge = typeof provider.codexmate_bridge === 'string' ? provider.codexmate_bridge.trim() : '';
|
|
14
|
+
if (bridge === 'openai') return true;
|
|
15
|
+
const url = String(provider.url || '');
|
|
16
|
+
return url.includes('/bridge/openai/');
|
|
17
|
+
}
|
|
@@ -107,6 +107,12 @@
|
|
|
107
107
|
:aria-pressed="(lang || 'zh') === 'en'"
|
|
108
108
|
:class="{ active: (lang || 'zh') === 'en' }"
|
|
109
109
|
@click="setLang('en')">EN</button>
|
|
110
|
+
<button
|
|
111
|
+
type="button"
|
|
112
|
+
class="lang-choice-btn"
|
|
113
|
+
:aria-pressed="(lang || 'zh') === 'ja'"
|
|
114
|
+
:class="{ active: (lang || 'zh') === 'ja' }"
|
|
115
|
+
@click="setLang('ja')">日本語</button>
|
|
110
116
|
</div>
|
|
111
117
|
</div>
|
|
112
118
|
|
|
@@ -297,6 +303,19 @@
|
|
|
297
303
|
<span>{{ t('side.system.settings.meta') }}</span>
|
|
298
304
|
</div>
|
|
299
305
|
</button>
|
|
306
|
+
<button
|
|
307
|
+
id="side-tab-trash"
|
|
308
|
+
data-main-tab="trash"
|
|
309
|
+
:aria-current="mainTab === 'trash' ? 'page' : null"
|
|
310
|
+
:class="['side-item', { active: isMainTabNavActive('trash') }]"
|
|
311
|
+
@pointerdown="onMainTabPointerDown('trash', $event)"
|
|
312
|
+
@click="onMainTabClick('trash', $event)">
|
|
313
|
+
<div class="side-item-title">回收站</div>
|
|
314
|
+
<div class="side-item-meta">
|
|
315
|
+
<span>已删除会话</span>
|
|
316
|
+
<span v-if="sessionTrashCount > 0" class="side-item-badge">{{ sessionTrashCount }}</span>
|
|
317
|
+
</div>
|
|
318
|
+
</button>
|
|
300
319
|
</div>
|
|
301
320
|
</div>
|
|
302
321
|
|
|
@@ -314,6 +333,12 @@
|
|
|
314
333
|
:aria-pressed="(lang || 'zh') === 'en'"
|
|
315
334
|
:class="{ active: (lang || 'zh') === 'en' }"
|
|
316
335
|
@click="setLang('en')">EN</button>
|
|
336
|
+
<button
|
|
337
|
+
type="button"
|
|
338
|
+
class="lang-choice-btn"
|
|
339
|
+
:aria-pressed="(lang || 'zh') === 'ja'"
|
|
340
|
+
:class="{ active: (lang || 'zh') === 'ja' }"
|
|
341
|
+
@click="setLang('ja')">日本語</button>
|
|
317
342
|
</div>
|
|
318
343
|
</div>
|
|
319
344
|
</aside>
|
|
@@ -45,8 +45,7 @@
|
|
|
45
45
|
<div class="selector-header">
|
|
46
46
|
<span class="selector-title">{{ t('claude.presetProviders') }}</span>
|
|
47
47
|
</div>
|
|
48
|
-
<div class="btn-group" style="flex-wrap: wrap; gap: 8px;">
|
|
49
|
-
<button type="button" class="btn-mini" @click="closeClaudeConfigModal(); showClaudeConfigModal = true">{{ t('claude.customConfig') }}</button>
|
|
48
|
+
<div class="btn-group" style="flex-wrap: wrap; gap: 8px; margin-top: 0;">
|
|
50
49
|
<button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Claude Official'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.anthropic.com'; newClaudeConfig.model = 'claude-sonnet-4'; showClaudeConfigModal = true">Claude Official</button>
|
|
51
50
|
<button type="button" class="btn-mini" @click="newClaudeConfig.name = 'DeepSeek'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://api.deepseek.com/anthropic'; newClaudeConfig.model = 'DeepSeek-V3.2'; showClaudeConfigModal = true">DeepSeek</button>
|
|
52
51
|
<button type="button" class="btn-mini" @click="newClaudeConfig.name = 'Zhipu GLM'; newClaudeConfig.apiKey = ''; newClaudeConfig.baseUrl = 'https://open.bigmodel.cn/api/anthropic'; newClaudeConfig.model = 'glm-5'; showClaudeConfigModal = true">Zhipu GLM</button>
|
|
@@ -136,6 +135,7 @@
|
|
|
136
135
|
</div>
|
|
137
136
|
</div>
|
|
138
137
|
|
|
138
|
+
|
|
139
139
|
<div class="card-list">
|
|
140
140
|
<div v-for="(config, name) in claudeConfigs" :key="name"
|
|
141
141
|
:class="['card', { active: currentClaudeConfig === name }]"
|
|
@@ -149,7 +149,8 @@
|
|
|
149
149
|
<div class="card-icon">{{ name.charAt(0).toUpperCase() }}</div>
|
|
150
150
|
<div class="card-content">
|
|
151
151
|
<div class="card-title">{{ name }}</div>
|
|
152
|
-
<div class="card-subtitle">{{ config.model || t('claude.model.unset') }}</div>
|
|
152
|
+
<div class="card-subtitle card-subtitle-model">{{ config.model || t('claude.model.unset') }}</div>
|
|
153
|
+
<div class="card-subtitle card-subtitle-url" v-if="config.baseUrl">{{ config.baseUrl }}</div>
|
|
153
154
|
</div>
|
|
154
155
|
</div>
|
|
155
156
|
<div class="card-trailing">
|
|
@@ -166,6 +167,12 @@
|
|
|
166
167
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
|
167
168
|
</svg>
|
|
168
169
|
</button>
|
|
170
|
+
<button class="card-action-btn" @click="openCloneClaudeConfigModal(name, config)" :aria-label="t('claude.action.cloneAria', { name })" :title="t('claude.action.clone')">
|
|
171
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
172
|
+
<rect x="9" y="9" width="13" height="13" rx="2"/>
|
|
173
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
|
174
|
+
</svg>
|
|
175
|
+
</button>
|
|
169
176
|
<button class="card-action-btn" :class="{ loading: claudeShareLoading[name] }" @click="copyClaudeShareCommand(name)" disabled :title="t('claude.action.shareDisabled')" :aria-label="t('config.shareCommand.aria')">
|
|
170
177
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
171
178
|
<path d="M4 12v7a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7"/>
|
|
@@ -38,6 +38,27 @@
|
|
|
38
38
|
{{ t('config.addProvider') }}
|
|
39
39
|
</button>
|
|
40
40
|
|
|
41
|
+
<!-- 服务预设 -->
|
|
42
|
+
<div class="selector-section" v-if="isCodexConfigMode && codexProviderTemplates.length">
|
|
43
|
+
<div class="selector-header">
|
|
44
|
+
<span class="selector-title">{{ t('config.providerTemplate.title') }}</span>
|
|
45
|
+
</div>
|
|
46
|
+
<div class="btn-group" style="flex-wrap: wrap; gap: 8px; margin-top: 0;">
|
|
47
|
+
<button
|
|
48
|
+
v-for="tpl in codexProviderTemplates"
|
|
49
|
+
:key="tpl.name"
|
|
50
|
+
type="button"
|
|
51
|
+
class="btn-mini"
|
|
52
|
+
@click="newProvider.name = tpl.name;
|
|
53
|
+
newProvider.url = tpl.url;
|
|
54
|
+
newProvider._suggestedModel = tpl.model || '';
|
|
55
|
+
newProvider.useTransform = !!tpl.useTransform;
|
|
56
|
+
showAddModal = true">
|
|
57
|
+
{{ tpl.label }}
|
|
58
|
+
</button>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
|
|
41
62
|
<!-- 模型选择器 -->
|
|
42
63
|
<div class="selector-section">
|
|
43
64
|
<div class="selector-header">
|
|
@@ -55,15 +76,20 @@
|
|
|
55
76
|
:disabled="codexModelsLoading"
|
|
56
77
|
>
|
|
57
78
|
<option v-if="codexModelsLoading" value="">{{ t('config.modelLoading') }}</option>
|
|
58
|
-
<option v-else v-for="model in
|
|
79
|
+
<option v-else v-for="model in codexModelOptions" :key="model" :value="model">{{ model }}</option>
|
|
59
80
|
</select>
|
|
60
81
|
<input
|
|
61
82
|
v-if="!codexModelsLoading && (modelsSource !== 'remote' || !modelsHasCurrent)"
|
|
62
83
|
class="model-input"
|
|
63
84
|
v-model="currentModel"
|
|
64
85
|
@blur="onModelChange"
|
|
86
|
+
@keyup.enter="onModelChange"
|
|
65
87
|
:placeholder="activeProviderModelPlaceholder"
|
|
88
|
+
:list="codexModelHasList ? 'codex-model-options' : null"
|
|
66
89
|
>
|
|
90
|
+
<datalist v-if="codexModelHasList" id="codex-model-options">
|
|
91
|
+
<option v-for="model in codexModelOptions" :key="model" :value="model"></option>
|
|
92
|
+
</datalist>
|
|
67
93
|
<div class="config-template-hint" v-if="modelsSource === 'unlimited'">
|
|
68
94
|
{{ t('config.models.unlimited') }}
|
|
69
95
|
</div>
|
|
@@ -204,14 +230,17 @@
|
|
|
204
230
|
role="button"
|
|
205
231
|
:aria-current="displayCurrentProvider === provider.name ? 'true' : null">
|
|
206
232
|
<div class="card-leading">
|
|
207
|
-
<div class="card-icon">{{ provider.name.charAt(0).toUpperCase() }}
|
|
233
|
+
<div class="card-icon">{{ provider.name.charAt(0).toUpperCase() }}<span v-if="isTransformProvider(provider)" class="card-icon-dot" title="通过内建转换适配"></span></div>
|
|
208
234
|
<div class="card-content">
|
|
209
235
|
<div class="card-title">
|
|
210
236
|
<span>{{ provider.name }}</span>
|
|
211
237
|
<span v-if="provider.readOnly" class="provider-readonly-badge">{{ t('config.badge.system') }}</span>
|
|
212
238
|
</div>
|
|
213
|
-
<div class="card-subtitle">
|
|
214
|
-
{{ provider.
|
|
239
|
+
<div class="card-subtitle card-subtitle-model">
|
|
240
|
+
{{ activeProviderModel(provider.name) || t('config.model.unset') }}
|
|
241
|
+
</div>
|
|
242
|
+
<div class="card-subtitle card-subtitle-url">
|
|
243
|
+
{{ displayProviderUrl(provider) || t('config.url.unset') }}
|
|
215
244
|
</div>
|
|
216
245
|
</div>
|
|
217
246
|
</div>
|
|
@@ -262,6 +291,17 @@
|
|
|
262
291
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
|
263
292
|
</svg>
|
|
264
293
|
</button>
|
|
294
|
+
<button
|
|
295
|
+
v-if="!provider.readOnly"
|
|
296
|
+
class="card-action-btn"
|
|
297
|
+
@click="openCloneProviderModal(provider)"
|
|
298
|
+
:aria-label="t('config.provider.clone.aria', { name: provider.name })"
|
|
299
|
+
:title="t('config.provider.clone')">
|
|
300
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
301
|
+
<rect x="9" y="9" width="13" height="13" rx="2"/>
|
|
302
|
+
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
|
|
303
|
+
</svg>
|
|
304
|
+
</button>
|
|
265
305
|
<button
|
|
266
306
|
v-if="!provider.readOnly"
|
|
267
307
|
class="card-action-btn delete"
|
|
@@ -176,7 +176,7 @@
|
|
|
176
176
|
<template v-else>
|
|
177
177
|
<div class="prompt-editor-head">
|
|
178
178
|
<div class="prompt-editor-title-row">
|
|
179
|
-
<input class="form-input prompt-editor-name" type="text" v-model.trim="
|
|
179
|
+
<input class="form-input prompt-editor-name" type="text" v-model.trim="promptTemplateDraftRaw.name" :disabled="promptTemplateDraft.isBuiltin" :placeholder="t('plugins.promptTemplates.editor.namePlaceholder')" :aria-label="t('plugins.promptTemplates.editor.nameAria')">
|
|
180
180
|
<div v-if="!promptTemplateDraft.isBuiltin" class="prompt-editor-actions">
|
|
181
181
|
<button type="button" class="btn-mini" @click="duplicatePromptTemplate" :disabled="pluginsLoading">{{ t('plugins.promptTemplates.editor.duplicate') }}</button>
|
|
182
182
|
<button type="button" class="btn-mini delete" @click="deletePromptTemplate" :disabled="pluginsLoading">{{ t('plugins.promptTemplates.editor.delete') }}</button>
|
|
@@ -189,14 +189,14 @@
|
|
|
189
189
|
<div v-if="promptTemplateDraft.isBuiltin && (promptTemplateDraft.createdBy || (promptTemplateDraft.maintainers && promptTemplateDraft.maintainers.length))" class="plugins-panel-note">
|
|
190
190
|
{{ t('plugins.meta.attribution', { createdBy: promptTemplateDraft.createdBy || '', maintainers: (promptTemplateDraft.maintainers || []).join(', ') }) }}
|
|
191
191
|
</div>
|
|
192
|
-
<input class="form-input" type="text" v-model.trim="
|
|
192
|
+
<input class="form-input" type="text" v-model.trim="promptTemplateDraftRaw.description" :disabled="promptTemplateDraft.isBuiltin" :placeholder="t('plugins.promptTemplates.editor.descPlaceholder')" :aria-label="t('plugins.promptTemplates.editor.descAria')">
|
|
193
193
|
</div>
|
|
194
194
|
|
|
195
195
|
<div class="prompt-editor-body">
|
|
196
196
|
<label class="form-label">{{ t('plugins.promptTemplates.editor.templateLabel') }}</label>
|
|
197
197
|
<textarea
|
|
198
198
|
class="form-input prompt-editor-textarea"
|
|
199
|
-
v-model="
|
|
199
|
+
v-model="promptTemplateDraftRaw.template"
|
|
200
200
|
:disabled="promptTemplateDraft.isBuiltin"
|
|
201
201
|
rows="10"
|
|
202
202
|
spellcheck="false"
|
|
@@ -210,7 +210,6 @@
|
|
|
210
210
|
<div class="plugins-panel-note">{{ t('plugins.promptTemplates.vars.hint') }}</div>
|
|
211
211
|
</div>
|
|
212
212
|
<div v-if="!promptTemplateDraft.isBuiltin" class="prompt-editor-actions">
|
|
213
|
-
<button type="button" class="btn-mini" @click="addPromptTemplateVariable" :disabled="pluginsLoading">{{ t('plugins.promptTemplates.vars.add') }}</button>
|
|
214
213
|
<button type="button" class="btn-mini" @click="resetPromptVariableValues" :disabled="pluginsLoading">{{ t('plugins.promptTemplates.vars.reset') }}</button>
|
|
215
214
|
</div>
|
|
216
215
|
</div>
|
|
@@ -252,28 +251,3 @@
|
|
|
252
251
|
style="display:none"
|
|
253
252
|
@change="handlePromptTemplatesImportChange">
|
|
254
253
|
|
|
255
|
-
<!-- 新增变量(Prompt Templates) -->
|
|
256
|
-
<div v-if="showPromptTemplateVarModal" class="modal-overlay" @click.self="closePromptTemplateVarModal">
|
|
257
|
-
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="add-prompt-var-modal-title">
|
|
258
|
-
<div class="modal-title" id="add-prompt-var-modal-title">{{ t('plugins.promptTemplates.varModal.title') }}</div>
|
|
259
|
-
|
|
260
|
-
<div class="form-group">
|
|
261
|
-
<label class="form-label">{{ t('plugins.promptTemplates.varModal.nameLabel') }}</label>
|
|
262
|
-
<input
|
|
263
|
-
ref="promptTemplateVarNameInput"
|
|
264
|
-
v-model.trim="promptTemplateVarDraftName"
|
|
265
|
-
:class="['form-input', { invalid: !!promptTemplateVarDraftError }]"
|
|
266
|
-
:placeholder="t('placeholder.varNameExample')"
|
|
267
|
-
autocomplete="off"
|
|
268
|
-
spellcheck="false"
|
|
269
|
-
@keydown.enter.prevent="confirmAddPromptTemplateVariable">
|
|
270
|
-
<div v-if="promptTemplateVarDraftError" class="form-hint form-error">{{ promptTemplateVarDraftError }}</div>
|
|
271
|
-
<div v-else class="form-hint">{{ t('hint.varNameRules') }}</div>
|
|
272
|
-
</div>
|
|
273
|
-
|
|
274
|
-
<div class="btn-group">
|
|
275
|
-
<button class="btn btn-cancel" @click="closePromptTemplateVarModal">{{ t('plugins.promptTemplates.varModal.cancel') }}</button>
|
|
276
|
-
<button class="btn btn-confirm" @click="confirmAddPromptTemplateVariable">{{ t('plugins.promptTemplates.varModal.add') }}</button>
|
|
277
|
-
</div>
|
|
278
|
-
</div>
|
|
279
|
-
</div>
|
|
@@ -97,16 +97,6 @@
|
|
|
97
97
|
</button>
|
|
98
98
|
</div>
|
|
99
99
|
</div>
|
|
100
|
-
<div class="session-toolbar-footer">
|
|
101
|
-
<label class="quick-option">
|
|
102
|
-
<input
|
|
103
|
-
type="checkbox"
|
|
104
|
-
v-model="sessionResumeWithYolo"
|
|
105
|
-
@change="onSessionResumeYoloChange"
|
|
106
|
-
>
|
|
107
|
-
{{ t('sessions.resumeYolo') }}
|
|
108
|
-
</label>
|
|
109
|
-
</div>
|
|
110
100
|
<div v-if="hasActiveSessionFilters()" class="session-filter-chips" :aria-label="t('sessions.filters.copyLink')">
|
|
111
101
|
<button
|
|
112
102
|
v-for="chip in getSessionFilterChips()"
|