codexmate 0.0.34 → 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.
- package/cli.js +74 -61
- package/package.json +2 -1
- package/web-ui/app.js +33 -2
- package/web-ui/index.html +1 -1
- package/web-ui/logic.sessions.mjs +6 -5
- package/web-ui/modules/app.computed.dashboard.mjs +4 -0
- package/web-ui/modules/app.computed.session.mjs +147 -6
- package/web-ui/modules/app.methods.claude-config.mjs +4 -0
- package/web-ui/modules/app.methods.navigation.mjs +32 -16
- package/web-ui/modules/app.methods.session-browser.mjs +7 -0
- package/web-ui/modules/app.methods.session-trash.mjs +30 -0
- package/web-ui/modules/i18n.dict.mjs +5 -0
- package/web-ui/modules/sessions-filters-url.mjs +65 -12
- package/web-ui/modules/skills.methods.mjs +31 -0
- package/web-ui/partials/index/layout-header.html +17 -12
- package/web-ui/partials/index/panel-config-claude.html +5 -3
- package/web-ui/partials/index/panel-config-codex.html +1 -1
- package/web-ui/partials/index/panel-market.html +76 -149
- package/web-ui/partials/index/panel-sessions.html +2 -2
- package/web-ui/partials/index/panel-settings.html +4 -2
- package/web-ui/partials/index/panel-usage.html +115 -68
- package/web-ui/res/vue.runtime.global.prod.js +7 -0
- package/web-ui/res/web-ui-render.precompiled.js +7274 -0
- package/web-ui/session-helpers.mjs +15 -4
- package/web-ui/source-bundle.cjs +73 -1
- package/web-ui/styles/base-theme.css +10 -0
- package/web-ui/styles/layout-shell.css +66 -27
- package/web-ui/styles/navigation-panels.css +8 -0
- package/web-ui/styles/responsive.css +50 -9
- package/web-ui/styles/sessions-usage.css +336 -319
- package/web-ui/styles/skills-market.css +294 -0
- package/web-ui/styles/titles-cards.css +14 -0
|
@@ -218,6 +218,36 @@ export function createSessionTrashMethods(options = {}) {
|
|
|
218
218
|
await this.switchSettingsTab(tab);
|
|
219
219
|
},
|
|
220
220
|
|
|
221
|
+
async onSettingsTabKeydown(event, tab) {
|
|
222
|
+
if (!event) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const tabs = ['general', 'data'];
|
|
226
|
+
const currentTab = this.normalizeSettingsTab(tab || this.settingsTab);
|
|
227
|
+
const currentIndex = Math.max(0, tabs.indexOf(currentTab));
|
|
228
|
+
let nextIndex = currentIndex;
|
|
229
|
+
if (event.key === 'ArrowRight' || event.key === 'ArrowDown') {
|
|
230
|
+
nextIndex = (currentIndex + 1) % tabs.length;
|
|
231
|
+
} else if (event.key === 'ArrowLeft' || event.key === 'ArrowUp') {
|
|
232
|
+
nextIndex = (currentIndex - 1 + tabs.length) % tabs.length;
|
|
233
|
+
} else if (event.key === 'Home') {
|
|
234
|
+
nextIndex = 0;
|
|
235
|
+
} else if (event.key === 'End') {
|
|
236
|
+
nextIndex = tabs.length - 1;
|
|
237
|
+
} else {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
event.preventDefault();
|
|
241
|
+
const nextTab = tabs[nextIndex];
|
|
242
|
+
await this.switchSettingsTab(nextTab);
|
|
243
|
+
const target = typeof document !== 'undefined'
|
|
244
|
+
? document.getElementById(`settings-tab-${nextTab}`)
|
|
245
|
+
: null;
|
|
246
|
+
if (target && typeof target.focus === 'function') {
|
|
247
|
+
target.focus();
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
|
|
221
251
|
async switchSettingsTab(tab, options = {}) {
|
|
222
252
|
const nextTab = this.normalizeSettingsTab(tab);
|
|
223
253
|
this.settingsTab = nextTab;
|
|
@@ -532,6 +532,7 @@ const DICT = Object.freeze({
|
|
|
532
532
|
'sessions.loadingList': '会话加载中...',
|
|
533
533
|
'sessions.empty': '暂无可用会话记录',
|
|
534
534
|
'sessions.unknownTime': '未知时间',
|
|
535
|
+
|
|
535
536
|
'sessions.query.placeholder.enabled': '关键词检索(支持 Codex/Claude/Gemini/CodeBuddy,例:claude code)',
|
|
536
537
|
'sessions.query.placeholder.disabled': '当前来源暂不支持关键词检索',
|
|
537
538
|
'sessions.pin': '置顶',
|
|
@@ -1602,6 +1603,8 @@ const DICT = Object.freeze({
|
|
|
1602
1603
|
'sessions.loadingList': 'セッション一覧を読み込み中...',
|
|
1603
1604
|
'sessions.empty': 'セッションがありません',
|
|
1604
1605
|
'sessions.unknownTime': '不明な時間',
|
|
1606
|
+
|
|
1607
|
+
|
|
1605
1608
|
'sessions.query.placeholder.enabled': 'セッションを検索...',
|
|
1606
1609
|
'sessions.query.placeholder.disabled': '現在のソースでは検索は利用できません',
|
|
1607
1610
|
'sessions.pin': 'ピン留め',
|
|
@@ -2658,6 +2661,8 @@ const DICT = Object.freeze({
|
|
|
2658
2661
|
'sessions.loadingList': 'Loading sessions...',
|
|
2659
2662
|
'sessions.empty': 'No sessions found',
|
|
2660
2663
|
'sessions.unknownTime': 'unknown time',
|
|
2664
|
+
|
|
2665
|
+
|
|
2661
2666
|
'sessions.query.placeholder.enabled': 'Search keywords (Codex/Claude/Gemini/CodeBuddy, e.g. claude code)',
|
|
2662
2667
|
'sessions.query.placeholder.disabled': 'Keyword search is not available for this source',
|
|
2663
2668
|
'sessions.pin': 'Pin',
|
|
@@ -55,20 +55,76 @@ export function applySessionsFilterUrlState(vm, state) {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
+
export function canonicalizeWebUiUrl(url) {
|
|
59
|
+
if (!url || typeof url !== 'object') return url;
|
|
60
|
+
if (url.pathname === '/web-ui/index.html') {
|
|
61
|
+
url.pathname = '/';
|
|
62
|
+
}
|
|
63
|
+
return url;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function canonicalizeWebUiHistoryUrl(value) {
|
|
67
|
+
if (typeof window === 'undefined' || !window.location) return value;
|
|
68
|
+
if (typeof value === 'undefined' || value === null) return value;
|
|
69
|
+
try {
|
|
70
|
+
const url = canonicalizeWebUiUrl(new URL(String(value), window.location.href));
|
|
71
|
+
return url && url.pathname === '/' ? url.href : value;
|
|
72
|
+
} catch (_) {
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function normalizeCurrentWebUiUrl() {
|
|
78
|
+
try {
|
|
79
|
+
const url = canonicalizeWebUiUrl(new URL(window.location.href));
|
|
80
|
+
if (url && url.href !== window.location.href) {
|
|
81
|
+
window.history.replaceState(null, '', url.href);
|
|
82
|
+
}
|
|
83
|
+
return url;
|
|
84
|
+
} catch (_) {
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function installWebUiUrlCanonicalization() {
|
|
90
|
+
if (typeof window === 'undefined' || !window.history) return false;
|
|
91
|
+
if (window.__codexmateWebUiUrlCanonicalizationInstalled) {
|
|
92
|
+
normalizeCurrentWebUiUrl();
|
|
93
|
+
return true;
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const originalReplaceState = window.history.replaceState;
|
|
97
|
+
const originalPushState = window.history.pushState;
|
|
98
|
+
if (typeof originalReplaceState === 'function') {
|
|
99
|
+
window.history.replaceState = function replaceState(state, title, url) {
|
|
100
|
+
return originalReplaceState.call(this, state, title, canonicalizeWebUiHistoryUrl(url));
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
if (typeof originalPushState === 'function') {
|
|
104
|
+
window.history.pushState = function pushState(state, title, url) {
|
|
105
|
+
return originalPushState.call(this, state, title, canonicalizeWebUiHistoryUrl(url));
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
window.__codexmateWebUiUrlCanonicalizationInstalled = true;
|
|
109
|
+
normalizeCurrentWebUiUrl();
|
|
110
|
+
return true;
|
|
111
|
+
} catch (_) {
|
|
112
|
+
normalizeCurrentWebUiUrl();
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
58
117
|
export function buildSessionsFilterShareUrl(vm) {
|
|
59
118
|
try {
|
|
60
|
-
|
|
61
|
-
|
|
119
|
+
// 使用干净的根路径作为基础 URL,避免把 /web-ui/index.html 或 /session 带进分享链接。
|
|
120
|
+
const baseUrl = window.location.origin + '/';
|
|
121
|
+
const url = canonicalizeWebUiUrl(new URL(baseUrl));
|
|
122
|
+
url.searchParams.set('tab', 'sessions');
|
|
62
123
|
url.searchParams.set('s_source', String(vm.sessionFilterSource || 'all'));
|
|
63
124
|
if (vm.sessionPathFilter) url.searchParams.set('s_path', String(vm.sessionPathFilter || ''));
|
|
64
|
-
else url.searchParams.delete('s_path');
|
|
65
125
|
if (vm.sessionQuery && isSessionQueryEnabled(vm.sessionFilterSource)) url.searchParams.set('s_query', String(vm.sessionQuery || ''));
|
|
66
|
-
else url.searchParams.delete('s_query');
|
|
67
126
|
if (vm.sessionRoleFilter && vm.sessionRoleFilter !== 'all') url.searchParams.set('s_role', String(vm.sessionRoleFilter || 'all'));
|
|
68
|
-
else url.searchParams.delete('s_role');
|
|
69
127
|
if (vm.sessionTimePreset && vm.sessionTimePreset !== 'all') url.searchParams.set('s_time', String(vm.sessionTimePreset || 'all'));
|
|
70
|
-
else url.searchParams.delete('s_time');
|
|
71
|
-
url.searchParams.set('tab', 'sessions');
|
|
72
128
|
return url.toString();
|
|
73
129
|
} catch (_) {
|
|
74
130
|
return '';
|
|
@@ -76,10 +132,7 @@ export function buildSessionsFilterShareUrl(vm) {
|
|
|
76
132
|
}
|
|
77
133
|
|
|
78
134
|
export function syncSessionsFilterUrl(vm) {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
try {
|
|
82
|
-
window.history.replaceState(null, '', url);
|
|
83
|
-
} catch (_) {}
|
|
135
|
+
// URL 保持静态,不同步状态到 URL
|
|
136
|
+
// 所有状态通过 localStorage 管理
|
|
84
137
|
}
|
|
85
138
|
|
|
@@ -477,6 +477,37 @@ export function createSkillsMethods({ api }) {
|
|
|
477
477
|
this.skillsDeleting = false;
|
|
478
478
|
await this.scanImportableSkills({ silent: true });
|
|
479
479
|
}
|
|
480
|
+
},
|
|
481
|
+
|
|
482
|
+
openSkillsMenu() {
|
|
483
|
+
// Open skills manager modal as menu
|
|
484
|
+
this.openSkillsManager();
|
|
485
|
+
},
|
|
486
|
+
|
|
487
|
+
async importSingleSkill(skill) {
|
|
488
|
+
if (this.skillsImporting || this.skillsZipImporting) return;
|
|
489
|
+
const key = this.buildSkillImportKey(skill);
|
|
490
|
+
this.skillsImporting = true;
|
|
491
|
+
try {
|
|
492
|
+
const res = await api('import-skills', {
|
|
493
|
+
targetApp: this.skillsTargetApp,
|
|
494
|
+
imports: [skill]
|
|
495
|
+
});
|
|
496
|
+
if (res && res.error) {
|
|
497
|
+
this.showMessage(res.error, 'error');
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
const imported = Array.isArray(res && res.imported) ? res.imported : [];
|
|
501
|
+
if (imported.length > 0) {
|
|
502
|
+
this.showMessage(`已导入 ${skill.displayName || skill.name}`, 'success');
|
|
503
|
+
await this.refreshSkillsList({ silent: true });
|
|
504
|
+
await this.scanImportableSkills({ silent: true });
|
|
505
|
+
}
|
|
506
|
+
} catch (e) {
|
|
507
|
+
this.showMessage('导入失败', 'error');
|
|
508
|
+
} finally {
|
|
509
|
+
this.skillsImporting = false;
|
|
510
|
+
}
|
|
480
511
|
}
|
|
481
512
|
};
|
|
482
513
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div id="app" class="container" v-cloak>
|
|
2
2
|
<div v-if="!sessionStandalone" class="top-tabs" role="tablist" :aria-label="t('nav.topTabs.aria')">
|
|
3
|
-
<button class="top-tab"
|
|
3
|
+
<button type="button" class="top-tab"
|
|
4
4
|
id="tab-dashboard"
|
|
5
5
|
role="tab"
|
|
6
6
|
data-main-tab="dashboard"
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
:class="{ active: isMainTabNavActive('dashboard') }"
|
|
11
11
|
@pointerdown="onMainTabPointerDown('dashboard', $event)"
|
|
12
12
|
@click="onMainTabClick('dashboard', $event)">{{ t('tab.dashboard') }}</button>
|
|
13
|
-
<button class="top-tab"
|
|
13
|
+
<button type="button" class="top-tab"
|
|
14
14
|
id="tab-docs"
|
|
15
15
|
role="tab"
|
|
16
16
|
data-main-tab="docs"
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
:class="{ active: isMainTabNavActive('docs') }"
|
|
21
21
|
@pointerdown="onMainTabPointerDown('docs', $event)"
|
|
22
22
|
@click="onMainTabClick('docs', $event)">{{ t('tab.docs') }}</button>
|
|
23
|
-
<button class="top-tab"
|
|
23
|
+
<button type="button" class="top-tab"
|
|
24
24
|
id="tab-config"
|
|
25
25
|
role="tab"
|
|
26
26
|
data-main-tab="config"
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
:class="{ active: isMainTabNavActive('config') }"
|
|
32
32
|
@pointerdown="onMainTabPointerDown('config', $event)"
|
|
33
33
|
@click="onMainTabClick('config', $event)">{{ t('tab.config') }}</button>
|
|
34
|
-
<button class="top-tab"
|
|
34
|
+
<button type="button" class="top-tab"
|
|
35
35
|
id="tab-sessions"
|
|
36
36
|
role="tab"
|
|
37
37
|
data-main-tab="sessions"
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
:class="{ active: isMainTabNavActive('sessions') }"
|
|
42
42
|
@pointerdown="onMainTabPointerDown('sessions', $event)"
|
|
43
43
|
@click="onMainTabClick('sessions', $event)">{{ t('tab.sessions') }}</button>
|
|
44
|
-
<button class="top-tab"
|
|
44
|
+
<button type="button" class="top-tab"
|
|
45
45
|
id="tab-usage"
|
|
46
46
|
role="tab"
|
|
47
47
|
data-main-tab="usage"
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
:class="{ active: isMainTabNavActive('usage') }"
|
|
52
52
|
@pointerdown="onMainTabPointerDown('usage', $event)"
|
|
53
53
|
@click="onMainTabClick('usage', $event)">{{ t('tab.usage') }}</button>
|
|
54
|
-
<button v-if="taskOrchestrationTabEnabled" class="top-tab"
|
|
54
|
+
<button v-if="taskOrchestrationTabEnabled" type="button" class="top-tab"
|
|
55
55
|
id="tab-orchestration"
|
|
56
56
|
role="tab"
|
|
57
57
|
data-main-tab="orchestration"
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
:class="{ active: isMainTabNavActive('orchestration') }"
|
|
62
62
|
@pointerdown="onMainTabPointerDown('orchestration', $event)"
|
|
63
63
|
@click="onMainTabClick('orchestration', $event)">{{ t('tab.orchestration') }}</button>
|
|
64
|
-
<button class="top-tab"
|
|
64
|
+
<button type="button" class="top-tab"
|
|
65
65
|
id="tab-market"
|
|
66
66
|
role="tab"
|
|
67
67
|
data-main-tab="market"
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
:class="{ active: isMainTabNavActive('market') }"
|
|
72
72
|
@pointerdown="onMainTabPointerDown('market', $event)"
|
|
73
73
|
@click="onMainTabClick('market', $event)">{{ t('tab.market') }}</button>
|
|
74
|
-
<button class="top-tab"
|
|
74
|
+
<button type="button" class="top-tab"
|
|
75
75
|
id="tab-plugins"
|
|
76
76
|
role="tab"
|
|
77
77
|
data-main-tab="plugins"
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
:class="{ active: isMainTabNavActive('plugins') }"
|
|
82
82
|
@pointerdown="onMainTabPointerDown('plugins', $event)"
|
|
83
83
|
@click="onMainTabClick('plugins', $event)">{{ t('tab.plugins') }}</button>
|
|
84
|
-
<button class="top-tab"
|
|
84
|
+
<button type="button" class="top-tab"
|
|
85
85
|
id="tab-settings"
|
|
86
86
|
role="tab"
|
|
87
87
|
data-main-tab="settings"
|
|
@@ -118,14 +118,13 @@
|
|
|
118
118
|
|
|
119
119
|
<div :class="['app-shell', { standalone: sessionStandalone }]">
|
|
120
120
|
<aside class="side-rail" v-if="!sessionStandalone">
|
|
121
|
-
<div class="brand-block">
|
|
121
|
+
<div class="brand-block" tabindex="0" @mouseenter="brandHovered = true" @mouseleave="brandHovered = false" @focus="brandHovered = true" @blur="brandHovered = false">
|
|
122
122
|
<div class="brand-head">
|
|
123
123
|
<img class="brand-logo" src="/res/logo-pack.webp" alt="Codex Mate logo">
|
|
124
124
|
<div class="brand-copy">
|
|
125
|
-
<div class="brand-kicker">Codex Mate
|
|
125
|
+
<div class="brand-kicker">Codex Mate<transition name="brand-version-fade"><span v-if="appVersion && brandHovered" class="brand-version"> v{{ appVersion }}</span></transition></div>
|
|
126
126
|
</div>
|
|
127
127
|
</div>
|
|
128
|
-
<div class="brand-subtitle">{{ t('brand.subtitle.localConfigSessionsWorkspace') }}</div>
|
|
129
128
|
</div>
|
|
130
129
|
|
|
131
130
|
<div class="side-rail-nav">
|
|
@@ -479,6 +478,12 @@
|
|
|
479
478
|
<span class="value">{{ installRegistryPreview || t('common.defaultOfficial') }}</span>
|
|
480
479
|
</div>
|
|
481
480
|
</div>
|
|
481
|
+
<div class="status-strip status-strip-placeholder" v-else-if="!sessionStandalone" aria-hidden="true">
|
|
482
|
+
<div class="status-chip">
|
|
483
|
+
<span class="label"> </span>
|
|
484
|
+
<span class="value"> </span>
|
|
485
|
+
</div>
|
|
486
|
+
</div>
|
|
482
487
|
<div
|
|
483
488
|
v-if="!sessionStandalone && mainTab === 'config' && isProviderConfigMode && forceCompactLayout && !loading && !initError && displayProvidersList.length > 1"
|
|
484
489
|
class="provider-fast-switch">
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
</div>
|
|
101
101
|
|
|
102
102
|
<div class="card-list">
|
|
103
|
-
<div :class="['card', { active: currentClaudeConfig === 'claude-local' }]" @click="applyClaudeLocalBridge()" @keydown.enter.self.prevent="applyClaudeLocalBridge()" @keydown.space.self.prevent="applyClaudeLocalBridge()" tabindex="0" role="button" :aria-current="currentClaudeConfig === 'claude-local' ? 'true' : null">
|
|
103
|
+
<div :class="['card', { active: currentClaudeConfig === 'claude-local', disabled: isClaudeLocalBridgeDisabled() }]" @click="isClaudeLocalBridgeDisabled() ? null : applyClaudeLocalBridge()" @keydown.enter.self.prevent="isClaudeLocalBridgeDisabled() ? null : applyClaudeLocalBridge()" @keydown.space.self.prevent="isClaudeLocalBridgeDisabled() ? null : applyClaudeLocalBridge()" :tabindex="isClaudeLocalBridgeDisabled() ? -1 : 0" role="button" :aria-current="currentClaudeConfig === 'claude-local' ? 'true' : null">
|
|
104
104
|
<div class="card-leading">
|
|
105
105
|
<div class="card-icon">L</div>
|
|
106
106
|
<div class="card-content">
|
|
@@ -108,8 +108,10 @@
|
|
|
108
108
|
<svg class="bridge-pool-summary-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="12" height="12"><circle cx="6" cy="6" r="2"/><circle cx="18" cy="6" r="2"/><circle cx="12" cy="18" r="2"/><path d="M6 8v4h6v4"/><path d="M18 8v4h-6v4"/></svg>
|
|
109
109
|
<span class="bridge-pool-summary-text">{{ t('claude.localBridge.enabled') }} {{ claudeLocalBridgeCandidateProviders().filter(cp => !isClaudeLocalBridgeExcluded(cp.name)).length }} / {{ claudeLocalBridgeCandidateProviders().length }}</span>
|
|
110
110
|
</div>
|
|
111
|
-
<div class="card-title">
|
|
112
|
-
|
|
111
|
+
<div class="card-title">
|
|
112
|
+
<span>local</span>
|
|
113
|
+
<span class="provider-readonly-badge">{{ t('config.badge.system') }}</span>
|
|
114
|
+
</div>
|
|
113
115
|
</div>
|
|
114
116
|
</div>
|
|
115
117
|
<div class="card-trailing">
|
|
@@ -120,7 +120,7 @@
|
|
|
120
120
|
</template>
|
|
121
121
|
|
|
122
122
|
<div v-if="!loading && !initError" class="card-list">
|
|
123
|
-
<div v-for="provider in displayProvidersList" :key="provider.name" :class="['card', { active: displayCurrentProvider === provider.name }]" @click="switchProvider(provider.name)" @keydown.enter.self.prevent="switchProvider(provider.name)" @keydown.space.self.prevent="switchProvider(provider.name)" tabindex="0" role="button" :aria-current="displayCurrentProvider === provider.name ? 'true' : null">
|
|
123
|
+
<div v-for="provider in displayProvidersList" :key="provider.name" :class="['card', { active: displayCurrentProvider === provider.name, disabled: provider.name === 'local' && isLocalProviderDisabled }]" @click="(provider.name === 'local' && isLocalProviderDisabled) ? null : switchProvider(provider.name)" @keydown.enter.self.prevent="(provider.name === 'local' && isLocalProviderDisabled) ? null : switchProvider(provider.name)" @keydown.space.self.prevent="(provider.name === 'local' && isLocalProviderDisabled) ? null : switchProvider(provider.name)" :tabindex="provider.name === 'local' && isLocalProviderDisabled ? -1 : 0" role="button" :aria-current="displayCurrentProvider === provider.name ? 'true' : null">
|
|
124
124
|
<div class="card-leading">
|
|
125
125
|
<div class="card-icon">{{ provider.name.charAt(0).toUpperCase() }}<span v-if="isTransformProvider(provider)" class="card-icon-dot" title="通过内建转换适配"></span></div>
|
|
126
126
|
<div class="card-content">
|
|
@@ -4,172 +4,99 @@
|
|
|
4
4
|
id="panel-market"
|
|
5
5
|
role="tabpanel"
|
|
6
6
|
aria-labelledby="tab-market">
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
7
|
+
<!-- Minimalist header with target switch -->
|
|
8
|
+
<div class="skills-minimal-header">
|
|
9
|
+
<div class="skills-header-left">
|
|
10
|
+
<span class="skills-header-title">{{ t('market.title') }}</span>
|
|
11
|
+
<div class="skills-target-switch" role="group" :aria-label="t('market.target.aria')">
|
|
12
|
+
<button
|
|
13
|
+
type="button"
|
|
14
|
+
:class="['skills-target-chip', { active: skillsTargetApp === 'codex' }]"
|
|
15
|
+
:aria-pressed="skillsTargetApp === 'codex'"
|
|
16
|
+
:disabled="loading || !!initError || skillsMarketBusy"
|
|
17
|
+
@click="setSkillsTargetApp('codex', { silent: false })">
|
|
18
|
+
Codex
|
|
19
19
|
</button>
|
|
20
|
-
<button
|
|
21
|
-
|
|
20
|
+
<button
|
|
21
|
+
type="button"
|
|
22
|
+
:class="['skills-target-chip', { active: skillsTargetApp === 'claude' }]"
|
|
23
|
+
:aria-pressed="skillsTargetApp === 'claude'"
|
|
24
|
+
:disabled="loading || !!initError || skillsMarketBusy"
|
|
25
|
+
@click="setSkillsTargetApp('claude', { silent: false })">
|
|
26
|
+
Claude Code
|
|
22
27
|
</button>
|
|
23
28
|
</div>
|
|
24
29
|
</div>
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
type="button"
|
|
29
|
-
:class="['market-target-chip', { active: skillsTargetApp === 'codex' }]"
|
|
30
|
-
:aria-pressed="skillsTargetApp === 'codex'"
|
|
31
|
-
:disabled="loading || !!initError || skillsMarketBusy"
|
|
32
|
-
@click="setSkillsTargetApp('codex', { silent: false })">
|
|
33
|
-
Codex
|
|
30
|
+
<div class="skills-header-actions">
|
|
31
|
+
<button type="button" class="btn-icon" @click="openSkillsMenu" :aria-label="t('common.menu')" :title="t('common.menu')">
|
|
32
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="20" height="20"><circle cx="12" cy="12" r="1"/><circle cx="12" cy="5" r="1"/><circle cx="12" cy="19" r="1"/></svg>
|
|
34
33
|
</button>
|
|
35
|
-
<button
|
|
36
|
-
type="button"
|
|
37
|
-
:class="['market-target-chip', { active: skillsTargetApp === 'claude' }]"
|
|
38
|
-
:aria-pressed="skillsTargetApp === 'claude'"
|
|
39
|
-
:disabled="loading || !!initError || skillsMarketBusy"
|
|
40
|
-
@click="setSkillsTargetApp('claude', { silent: false })">
|
|
41
|
-
Claude Code
|
|
42
|
-
</button>
|
|
43
|
-
</div>
|
|
44
|
-
|
|
45
|
-
<div class="skills-root-box market-root-box">{{ skillsRootPath || skillsDefaultRootPath }}</div>
|
|
46
|
-
|
|
47
|
-
<div class="skills-summary-strip market-summary-strip">
|
|
48
|
-
<div class="skills-summary-item">
|
|
49
|
-
<span class="skills-summary-label">{{ t('market.summary.target') }}</span>
|
|
50
|
-
<strong class="skills-summary-value">{{ skillsTargetLabel }}</strong>
|
|
51
|
-
</div>
|
|
52
|
-
<div class="skills-summary-item">
|
|
53
|
-
<span class="skills-summary-label">{{ t('market.summary.total') }}</span>
|
|
54
|
-
<strong class="skills-summary-value">{{ skillsList.length }}</strong>
|
|
55
|
-
</div>
|
|
56
|
-
<div class="skills-summary-item">
|
|
57
|
-
<span class="skills-summary-label">{{ t('market.summary.configured') }}</span>
|
|
58
|
-
<strong class="skills-summary-value">{{ skillsConfiguredCount }}</strong>
|
|
59
|
-
</div>
|
|
60
|
-
<div class="skills-summary-item">
|
|
61
|
-
<span class="skills-summary-label">{{ t('market.summary.missing') }}</span>
|
|
62
|
-
<strong class="skills-summary-value">{{ skillsMissingSkillFileCount }}</strong>
|
|
63
|
-
</div>
|
|
64
|
-
<div class="skills-summary-item">
|
|
65
|
-
<span class="skills-summary-label">{{ t('market.summary.importable') }}</span>
|
|
66
|
-
<strong class="skills-summary-value">{{ skillsImportList.length }}</strong>
|
|
67
|
-
</div>
|
|
68
|
-
<div class="skills-summary-item">
|
|
69
|
-
<span class="skills-summary-label">{{ t('market.summary.importableDirect') }}</span>
|
|
70
|
-
<strong class="skills-summary-value">{{ skillsImportConfiguredCount }}</strong>
|
|
71
|
-
</div>
|
|
72
34
|
</div>
|
|
73
35
|
</div>
|
|
74
36
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
</
|
|
86
|
-
<div v-if="skillsLoading && !skillsMarketLocalLoadedOnce" class="skills-empty-state">{{ t('market.local.loading') }}</div>
|
|
87
|
-
<div v-else-if="skillsList.length === 0" class="skills-empty-state">{{ t('market.local.empty') }}</div>
|
|
88
|
-
<div v-else class="market-preview-list">
|
|
89
|
-
<div v-for="skill in skillsMarketInstalledPreview" :key="'market-local-' + skill.name" class="market-preview-item">
|
|
90
|
-
<div class="market-preview-main">
|
|
91
|
-
<div class="market-preview-title">{{ skill.displayName || skill.name }}</div>
|
|
92
|
-
<div class="market-preview-meta">{{ skill.description || skill.path }}</div>
|
|
93
|
-
</div>
|
|
94
|
-
<span :class="['pill', skill.hasSkillFile ? 'configured' : 'empty']">
|
|
95
|
-
{{ skill.hasSkillFile ? t('market.pill.verified') : t('market.pill.missingSkill') }}
|
|
96
|
-
</span>
|
|
97
|
-
</div>
|
|
98
|
-
</div>
|
|
37
|
+
<!-- Installed skills panel -->
|
|
38
|
+
<div class="skills-flow-panel">
|
|
39
|
+
<div class="skills-flow-header">
|
|
40
|
+
<div class="skills-flow-title-wrap">
|
|
41
|
+
<span class="skills-flow-title">{{ t('market.installed.title') }}</span>
|
|
42
|
+
<span class="skills-flow-count">{{ skillsList.length }}</span>
|
|
43
|
+
</div>
|
|
44
|
+
<button type="button" class="btn-mini" @click="refreshSkillsList({ silent: false })" :disabled="loading || !!initError || skillsMarketBusy">
|
|
45
|
+
<svg v-if="!skillsLoading" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" width="14" height="14"><path d="M17 1l4 4-4 4"/><path d="M3 11V9a4 4 0 014-4h14"/><path d="M3 19l-4-4 4-4"/><path d="M17 9v2a4 4 0 01-4 4H3"/></svg>
|
|
46
|
+
<svg v-else viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" width="14" height="14" class="spin"><path d="M10 2v4m0 8v4M2 10h4m8 0h4m-3.5-6.5l2.5 2.5M4 4l2.5 2.5M16 16l-2.5-2.5M4 16l2.5-2.5"/></svg>
|
|
47
|
+
</button>
|
|
99
48
|
</div>
|
|
100
|
-
|
|
101
|
-
<div class="skills-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
<
|
|
106
|
-
|
|
107
|
-
<button type="button" class="btn-mini" @click="scanImportableSkills({ silent: false })" :disabled="loading || !!initError || skillsMarketBusy">
|
|
108
|
-
{{ skillsScanningImports ? t('market.import.scanning') : t('market.import.scan') }}
|
|
109
|
-
</button>
|
|
110
|
-
</div>
|
|
111
|
-
<div v-if="skillsScanningImports && !skillsMarketImportLoadedOnce" class="skills-empty-state">{{ t('market.import.loading') }}</div>
|
|
112
|
-
<div v-else-if="skillsImportList.length === 0" class="skills-empty-state">{{ t('market.import.empty') }}</div>
|
|
113
|
-
<div v-else class="market-preview-list">
|
|
114
|
-
<div v-for="skill in skillsMarketImportPreview" :key="'market-import-' + buildSkillImportKey(skill)" class="market-preview-item">
|
|
115
|
-
<div class="market-preview-main">
|
|
116
|
-
<div class="market-preview-title">{{ skill.displayName || skill.name }}</div>
|
|
117
|
-
<div class="market-preview-meta">{{ skill.sourceLabel }} · {{ skill.sourcePath }}</div>
|
|
118
|
-
</div>
|
|
119
|
-
<span :class="['pill', skill.hasSkillFile ? 'configured' : 'empty']">
|
|
120
|
-
{{ skill.hasSkillFile ? t('market.pill.importableDirect') : t('market.pill.importMissing') }}
|
|
121
|
-
</span>
|
|
49
|
+
<div v-if="skillsLoading && !skillsMarketLocalLoadedOnce" class="skills-flow-loading">{{ t('market.local.loading') }}</div>
|
|
50
|
+
<div v-else-if="skillsList.length === 0" class="skills-flow-empty">{{ t('market.local.empty') }}</div>
|
|
51
|
+
<div v-else class="skills-flow-list">
|
|
52
|
+
<div v-for="skill in skillsList" :key="'skill-' + skill.name" class="skills-flow-item" :class="{ 'has-issue': !skill.hasSkillFile }">
|
|
53
|
+
<div class="skills-flow-main">
|
|
54
|
+
<span class="skills-flow-name">{{ skill.displayName || skill.name }}</span>
|
|
55
|
+
<span class="skills-flow-path">{{ skill.path }}</span>
|
|
122
56
|
</div>
|
|
57
|
+
<span :class="['skills-flow-status', skill.hasSkillFile ? 'success' : 'warning']">
|
|
58
|
+
{{ skill.hasSkillFile ? t('market.pill.verified') : t('market.pill.missingSkill') }}
|
|
59
|
+
</span>
|
|
123
60
|
</div>
|
|
124
61
|
</div>
|
|
62
|
+
</div>
|
|
125
63
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
</
|
|
132
|
-
</div>
|
|
133
|
-
<
|
|
134
|
-
<
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
64
|
+
<!-- Importable skills panel -->
|
|
65
|
+
<div class="skills-flow-panel">
|
|
66
|
+
<div class="skills-flow-header">
|
|
67
|
+
<div class="skills-flow-title-wrap">
|
|
68
|
+
<span class="skills-flow-title">{{ t('market.import.title') }}</span>
|
|
69
|
+
<span class="skills-flow-count">{{ skillsImportList.length }}</span>
|
|
70
|
+
</div>
|
|
71
|
+
<button type="button" class="btn-mini" @click="scanImportableSkills({ silent: false })" :disabled="loading || !!initError || skillsMarketBusy">
|
|
72
|
+
<svg v-if="!skillsScanningImports" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" width="14" height="14"><path d="M17 1l4 4-4 4"/><path d="M3 11V9a4 4 0 014-4h14"/><path d="M3 19l-4-4 4-4"/><path d="M17 9v2a4 4 0 01-4 4H3"/></svg>
|
|
73
|
+
<svg v-else viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" width="14" height="14" class="spin"><path d="M10 2v4m0 8v4M2 10h4m8 0h4m-3.5-6.5l2.5 2.5M4 4l2.5 2.5M16 16l-2.5-2.5M4 16l2.5-2.5"/></svg>
|
|
74
|
+
</button>
|
|
75
|
+
</div>
|
|
76
|
+
<div v-if="skillsScanningImports && !skillsMarketImportLoadedOnce" class="skills-flow-loading">{{ t('market.import.loading') }}</div>
|
|
77
|
+
<div v-else-if="skillsImportList.length === 0" class="skills-flow-empty">{{ t('market.import.empty') }}</div>
|
|
78
|
+
<div v-else>
|
|
79
|
+
<!-- Quick actions -->
|
|
80
|
+
<div class="skills-flow-actions">
|
|
81
|
+
<button type="button" class="skills-action-btn" @click="openSkillsManager" :disabled="loading || !!initError || skillsMarketBusy">
|
|
82
|
+
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" width="16" height="16"><rect x="3" y="3" width="14" height="14" rx="3"/><path d="M7 7h6M7 10h4"/></svg>
|
|
83
|
+
{{ t('market.action.manage.title') }}
|
|
141
84
|
</button>
|
|
142
|
-
<button type="button" class="
|
|
143
|
-
<
|
|
144
|
-
|
|
85
|
+
<button type="button" class="skills-action-btn" @click="triggerSkillsZipImport" :disabled="loading || !!initError || skillsMarketBusy">
|
|
86
|
+
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" width="16" height="16"><path d="M4 14l3-3 3 3"/><path d="M7 4v7"/><rect x="12" y="4" width="5" height="12" rx="1"/></svg>
|
|
87
|
+
{{ t('market.action.zipImport.title') }}
|
|
145
88
|
</button>
|
|
146
89
|
</div>
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
</div>
|
|
154
|
-
</div>
|
|
155
|
-
<div class="market-preview-list">
|
|
156
|
-
<div class="market-preview-item">
|
|
157
|
-
<div class="market-preview-main">
|
|
158
|
-
<div class="market-preview-title">{{ t('market.help.target.title') }}</div>
|
|
159
|
-
<div class="market-preview-meta">{{ t('market.help.target.copy', { target: skillsTargetLabel }) }}</div>
|
|
160
|
-
</div>
|
|
161
|
-
</div>
|
|
162
|
-
<div class="market-preview-item">
|
|
163
|
-
<div class="market-preview-main">
|
|
164
|
-
<div class="market-preview-title">{{ t('market.help.crossImport.title') }}</div>
|
|
165
|
-
<div class="market-preview-meta">{{ t('market.help.crossImport.copy') }}</div>
|
|
166
|
-
</div>
|
|
167
|
-
</div>
|
|
168
|
-
<div class="market-preview-item">
|
|
169
|
-
<div class="market-preview-main">
|
|
170
|
-
<div class="market-preview-title">{{ t('market.help.zipImport.title') }}</div>
|
|
171
|
-
<div class="market-preview-meta">{{ t('market.help.zipImport.copy') }}</div>
|
|
90
|
+
<!-- Import list -->
|
|
91
|
+
<div class="skills-flow-list">
|
|
92
|
+
<div v-for="skill in skillsImportList" :key="'import-' + buildSkillImportKey(skill)" class="skills-flow-item">
|
|
93
|
+
<div class="skills-flow-main">
|
|
94
|
+
<span class="skills-flow-name">{{ skill.displayName || skill.name }}</span>
|
|
95
|
+
<span class="skills-flow-meta">{{ skill.sourceLabel }}</span>
|
|
172
96
|
</div>
|
|
97
|
+
<button type="button" class="skills-flow-add" @click="importSingleSkill(skill)" :disabled="loading || !!initError || skillsMarketBusy">
|
|
98
|
+
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" width="16" height="16"><path d="M10 4v6m3-3l-3 3-3-3"/></svg>
|
|
99
|
+
</button>
|
|
173
100
|
</div>
|
|
174
101
|
</div>
|
|
175
102
|
</div>
|