codexmate 0.0.16 → 0.0.18
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.en.md +42 -24
- package/README.md +42 -24
- package/cli.js +1457 -157
- package/lib/text-diff.js +303 -0
- package/package.json +2 -2
- package/web-ui/app.js +1946 -247
- package/web-ui/index.html +321 -78
- package/web-ui/logic.mjs +503 -13
- package/web-ui/modules/skills.methods.mjs +7 -1
- package/web-ui/session-helpers.mjs +350 -0
- package/web-ui/styles.css +679 -31
package/web-ui/index.html
CHANGED
|
@@ -56,48 +56,56 @@
|
|
|
56
56
|
<button class="top-tab"
|
|
57
57
|
id="tab-config-codex"
|
|
58
58
|
role="tab"
|
|
59
|
+
data-main-tab="config"
|
|
60
|
+
data-config-mode="codex"
|
|
59
61
|
:tabindex="mainTab === 'config' && configMode === 'codex' ? 0 : -1"
|
|
60
62
|
:aria-selected="mainTab === 'config' && configMode === 'codex'"
|
|
61
|
-
:aria-pressed="mainTab === 'config' && configMode === 'codex'"
|
|
62
63
|
aria-controls="panel-config-provider"
|
|
63
|
-
:class="{ active:
|
|
64
|
-
@
|
|
64
|
+
:class="{ active: isConfigModeNavActive('codex') }"
|
|
65
|
+
@pointerdown="onConfigTabPointerDown('codex', $event)"
|
|
66
|
+
@click="onConfigTabClick('codex', $event)">Codex 配置</button>
|
|
65
67
|
<button class="top-tab"
|
|
66
68
|
id="tab-config-claude"
|
|
67
69
|
role="tab"
|
|
70
|
+
data-main-tab="config"
|
|
71
|
+
data-config-mode="claude"
|
|
68
72
|
:tabindex="mainTab === 'config' && configMode === 'claude' ? 0 : -1"
|
|
69
73
|
:aria-selected="mainTab === 'config' && configMode === 'claude'"
|
|
70
|
-
:aria-pressed="mainTab === 'config' && configMode === 'claude'"
|
|
71
74
|
aria-controls="panel-config-claude"
|
|
72
|
-
:class="{ active:
|
|
73
|
-
@
|
|
75
|
+
:class="{ active: isConfigModeNavActive('claude') }"
|
|
76
|
+
@pointerdown="onConfigTabPointerDown('claude', $event)"
|
|
77
|
+
@click="onConfigTabClick('claude', $event)">Claude Code 配置</button>
|
|
74
78
|
<button class="top-tab"
|
|
75
79
|
id="tab-config-openclaw"
|
|
76
80
|
role="tab"
|
|
81
|
+
data-main-tab="config"
|
|
82
|
+
data-config-mode="openclaw"
|
|
77
83
|
:tabindex="mainTab === 'config' && configMode === 'openclaw' ? 0 : -1"
|
|
78
84
|
:aria-selected="mainTab === 'config' && configMode === 'openclaw'"
|
|
79
|
-
:aria-pressed="mainTab === 'config' && configMode === 'openclaw'"
|
|
80
85
|
aria-controls="panel-config-openclaw"
|
|
81
|
-
:class="{ active:
|
|
82
|
-
@
|
|
86
|
+
:class="{ active: isConfigModeNavActive('openclaw') }"
|
|
87
|
+
@pointerdown="onConfigTabPointerDown('openclaw', $event)"
|
|
88
|
+
@click="onConfigTabClick('openclaw', $event)">OpenClaw 配置</button>
|
|
83
89
|
<button class="top-tab"
|
|
84
90
|
id="tab-sessions"
|
|
85
91
|
role="tab"
|
|
92
|
+
data-main-tab="sessions"
|
|
86
93
|
:tabindex="mainTab === 'sessions' ? 0 : -1"
|
|
87
94
|
:aria-selected="mainTab === 'sessions'"
|
|
88
|
-
:aria-pressed="mainTab === 'sessions'"
|
|
89
95
|
aria-controls="panel-sessions"
|
|
90
|
-
:class="{ active:
|
|
91
|
-
@
|
|
96
|
+
:class="{ active: isMainTabNavActive('sessions') }"
|
|
97
|
+
@pointerdown="onMainTabPointerDown('sessions', $event)"
|
|
98
|
+
@click="onMainTabClick('sessions', $event)">会话浏览</button>
|
|
92
99
|
<button class="top-tab"
|
|
93
100
|
id="tab-settings"
|
|
94
101
|
role="tab"
|
|
102
|
+
data-main-tab="settings"
|
|
95
103
|
:tabindex="mainTab === 'settings' ? 0 : -1"
|
|
96
104
|
:aria-selected="mainTab === 'settings'"
|
|
97
|
-
:aria-pressed="mainTab === 'settings'"
|
|
98
105
|
aria-controls="panel-settings"
|
|
99
|
-
:class="{ active:
|
|
100
|
-
@
|
|
106
|
+
:class="{ active: isMainTabNavActive('settings') }"
|
|
107
|
+
@pointerdown="onMainTabPointerDown('settings', $event)"
|
|
108
|
+
@click="onMainTabClick('settings', $event)">设置</button>
|
|
101
109
|
</div>
|
|
102
110
|
|
|
103
111
|
<div :class="['app-shell', { standalone: sessionStandalone }]">
|
|
@@ -137,12 +145,14 @@
|
|
|
137
145
|
<button
|
|
138
146
|
role="tab"
|
|
139
147
|
id="side-tab-config-codex"
|
|
148
|
+
data-main-tab="config"
|
|
149
|
+
data-config-mode="codex"
|
|
140
150
|
aria-controls="panel-config-provider"
|
|
141
151
|
:tabindex="mainTab === 'config' && configMode === 'codex' ? 0 : -1"
|
|
142
152
|
:aria-selected="mainTab === 'config' && configMode === 'codex'"
|
|
143
|
-
:
|
|
144
|
-
|
|
145
|
-
@click="
|
|
153
|
+
:class="['side-item', { active: isConfigModeNavActive('codex') }]"
|
|
154
|
+
@pointerdown="onConfigTabPointerDown('codex', $event)"
|
|
155
|
+
@click="onConfigTabClick('codex', $event)">
|
|
146
156
|
<div class="side-item-title">Codex 配置</div>
|
|
147
157
|
<div class="side-item-meta">
|
|
148
158
|
<span>提供商 / 模型</span>
|
|
@@ -152,12 +162,14 @@
|
|
|
152
162
|
<button
|
|
153
163
|
role="tab"
|
|
154
164
|
id="side-tab-config-claude"
|
|
165
|
+
data-main-tab="config"
|
|
166
|
+
data-config-mode="claude"
|
|
155
167
|
aria-controls="panel-config-claude"
|
|
156
168
|
:tabindex="mainTab === 'config' && configMode === 'claude' ? 0 : -1"
|
|
157
169
|
:aria-selected="mainTab === 'config' && configMode === 'claude'"
|
|
158
|
-
:
|
|
159
|
-
|
|
160
|
-
@click="
|
|
170
|
+
:class="['side-item', { active: isConfigModeNavActive('claude') }]"
|
|
171
|
+
@pointerdown="onConfigTabPointerDown('claude', $event)"
|
|
172
|
+
@click="onConfigTabClick('claude', $event)">
|
|
161
173
|
<div class="side-item-title">Claude Code 配置</div>
|
|
162
174
|
<div class="side-item-meta">
|
|
163
175
|
<span>Base URL / Key</span>
|
|
@@ -167,12 +179,14 @@
|
|
|
167
179
|
<button
|
|
168
180
|
role="tab"
|
|
169
181
|
id="side-tab-config-openclaw"
|
|
182
|
+
data-main-tab="config"
|
|
183
|
+
data-config-mode="openclaw"
|
|
170
184
|
aria-controls="panel-config-openclaw"
|
|
171
185
|
:tabindex="mainTab === 'config' && configMode === 'openclaw' ? 0 : -1"
|
|
172
186
|
:aria-selected="mainTab === 'config' && configMode === 'openclaw'"
|
|
173
|
-
:
|
|
174
|
-
|
|
175
|
-
@click="
|
|
187
|
+
:class="['side-item', { active: isConfigModeNavActive('openclaw') }]"
|
|
188
|
+
@pointerdown="onConfigTabPointerDown('openclaw', $event)"
|
|
189
|
+
@click="onConfigTabClick('openclaw', $event)">
|
|
176
190
|
<div class="side-item-title">OpenClaw 配置</div>
|
|
177
191
|
<div class="side-item-meta">
|
|
178
192
|
<span>JSON5 / Workspace</span>
|
|
@@ -186,12 +200,13 @@
|
|
|
186
200
|
<button
|
|
187
201
|
role="tab"
|
|
188
202
|
id="side-tab-sessions"
|
|
203
|
+
data-main-tab="sessions"
|
|
189
204
|
aria-controls="panel-sessions"
|
|
190
205
|
:tabindex="mainTab === 'sessions' ? 0 : -1"
|
|
191
206
|
:aria-selected="mainTab === 'sessions'"
|
|
192
|
-
:
|
|
193
|
-
|
|
194
|
-
@click="
|
|
207
|
+
:class="['side-item', { active: isMainTabNavActive('sessions') }]"
|
|
208
|
+
@pointerdown="onMainTabPointerDown('sessions', $event)"
|
|
209
|
+
@click="onMainTabClick('sessions', $event)">
|
|
195
210
|
<div class="side-item-title">会话浏览</div>
|
|
196
211
|
<div class="side-item-meta">
|
|
197
212
|
<span>快速预览 / 导出</span>
|
|
@@ -205,12 +220,13 @@
|
|
|
205
220
|
<button
|
|
206
221
|
role="tab"
|
|
207
222
|
id="side-tab-settings"
|
|
223
|
+
data-main-tab="settings"
|
|
208
224
|
aria-controls="panel-settings"
|
|
209
225
|
:tabindex="mainTab === 'settings' ? 0 : -1"
|
|
210
226
|
:aria-selected="mainTab === 'settings'"
|
|
211
|
-
:
|
|
212
|
-
|
|
213
|
-
@click="
|
|
227
|
+
:class="['side-item', { active: isMainTabNavActive('settings') }]"
|
|
228
|
+
@pointerdown="onMainTabPointerDown('settings', $event)"
|
|
229
|
+
@click="onMainTabClick('settings', $event)">
|
|
214
230
|
<div class="side-item-title">设置</div>
|
|
215
231
|
<div class="side-item-meta">
|
|
216
232
|
<span>数据管理 / 下载</span>
|
|
@@ -282,15 +298,29 @@
|
|
|
282
298
|
<span class="value">{{ sessionsList.length }}</span>
|
|
283
299
|
</div>
|
|
284
300
|
</div>
|
|
301
|
+
<div
|
|
302
|
+
v-if="!sessionStandalone && mainTab === 'config' && isProviderConfigMode && forceCompactLayout && !loading && !initError && providersList.length > 1"
|
|
303
|
+
class="provider-fast-switch">
|
|
304
|
+
<label class="provider-fast-switch-label" for="provider-fast-switch-select">快速切换提供商</label>
|
|
305
|
+
<select
|
|
306
|
+
id="provider-fast-switch-select"
|
|
307
|
+
class="provider-fast-switch-select"
|
|
308
|
+
:value="currentProvider"
|
|
309
|
+
@change="quickSwitchProvider($event.target.value)">
|
|
310
|
+
<option v-for="provider in providersList" :key="'quick-switch-' + provider.name" :value="provider.name">
|
|
311
|
+
{{ provider.name }}
|
|
312
|
+
</option>
|
|
313
|
+
</select>
|
|
314
|
+
</div>
|
|
285
315
|
|
|
286
316
|
<div v-if="false && mainTab === 'config' && !sessionStandalone" class="config-subtabs">
|
|
287
|
-
<button :class="['config-subtab', { active: configMode === 'codex' }]" @click="
|
|
317
|
+
<button :class="['config-subtab', { active: configMode === 'codex' }]" @click="onConfigTabClick('codex', $event)">
|
|
288
318
|
Codex 配置
|
|
289
319
|
</button>
|
|
290
|
-
<button :class="['config-subtab', { active: configMode === 'claude' }]" @click="
|
|
320
|
+
<button :class="['config-subtab', { active: configMode === 'claude' }]" @click="onConfigTabClick('claude', $event)">
|
|
291
321
|
Claude Code 配置
|
|
292
322
|
</button>
|
|
293
|
-
<button :class="['config-subtab', { active: configMode === 'openclaw' }]" @click="
|
|
323
|
+
<button :class="['config-subtab', { active: configMode === 'openclaw' }]" @click="onConfigTabClick('openclaw', $event)">
|
|
294
324
|
OpenClaw 配置
|
|
295
325
|
</button>
|
|
296
326
|
</div>
|
|
@@ -555,10 +585,11 @@
|
|
|
555
585
|
<button
|
|
556
586
|
v-if="!provider.readOnly"
|
|
557
587
|
class="card-action-btn"
|
|
558
|
-
:class="{ loading: providerShareLoading[provider.name]
|
|
559
|
-
|
|
588
|
+
:class="{ loading: providerShareLoading[provider.name] }"
|
|
589
|
+
disabled
|
|
560
590
|
@click="copyProviderShareCommand(provider)"
|
|
561
|
-
|
|
591
|
+
title="分享导入命令(暂时禁用)"
|
|
592
|
+
aria-label="Share import command">
|
|
562
593
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
563
594
|
<path d="M4 12v7a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7"/>
|
|
564
595
|
<path d="M16 6l-4-4-4 4"/>
|
|
@@ -672,7 +703,7 @@
|
|
|
672
703
|
<path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/>
|
|
673
704
|
</svg>
|
|
674
705
|
</button>
|
|
675
|
-
<button class="card-action-btn" :class="{ loading: claudeShareLoading[name] }" @click="copyClaudeShareCommand(name)" title="
|
|
706
|
+
<button class="card-action-btn" :class="{ loading: claudeShareLoading[name] }" @click="copyClaudeShareCommand(name)" disabled title="分享导入命令(暂时禁用)" aria-label="Share import command">
|
|
676
707
|
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
|
677
708
|
<path d="M4 12v7a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7"/>
|
|
678
709
|
<path d="M16 6l-4-4-4 4"/>
|
|
@@ -871,14 +902,16 @@
|
|
|
871
902
|
</div>
|
|
872
903
|
|
|
873
904
|
<div v-else :class="['session-layout', { 'session-standalone': sessionStandalone }]">
|
|
874
|
-
<div v-if="!sessionStandalone" class="session-list">
|
|
905
|
+
<div v-if="!sessionStandalone && sessionListRenderEnabled" class="session-list">
|
|
875
906
|
<div
|
|
876
|
-
v-for="session in
|
|
907
|
+
v-for="session in sortedSessionsList"
|
|
877
908
|
:key="session.source + '-' + session.sessionId + '-' + session.filePath"
|
|
909
|
+
v-memo="[activeSessionExportKey === getSessionExportKey(session), session.messageCount, session.updatedAt, session.title, session.sourceLabel, isSessionPinned(session), sessionsLoading]"
|
|
878
910
|
:class="[
|
|
879
911
|
'session-item',
|
|
880
912
|
{
|
|
881
|
-
active:
|
|
913
|
+
active: activeSessionExportKey === getSessionExportKey(session),
|
|
914
|
+
pinned: isSessionPinned(session)
|
|
882
915
|
}
|
|
883
916
|
]"
|
|
884
917
|
@click="selectSession(session)">
|
|
@@ -888,6 +921,20 @@
|
|
|
888
921
|
<span class="session-count-badge">{{ session.messageCount ?? 0 }}</span>
|
|
889
922
|
</div>
|
|
890
923
|
<div class="session-item-actions">
|
|
924
|
+
<button
|
|
925
|
+
class="session-item-copy session-item-pin"
|
|
926
|
+
@click.stop="toggleSessionPin(session)"
|
|
927
|
+
:disabled="sessionsLoading"
|
|
928
|
+
:aria-label="isSessionPinned(session) ? '取消置顶' : '置顶'"
|
|
929
|
+
:title="isSessionPinned(session) ? '取消置顶' : '置顶'"
|
|
930
|
+
:aria-pressed="isSessionPinned(session)">
|
|
931
|
+
<svg v-if="isSessionPinned(session)" class="pin-icon" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="1.6">
|
|
932
|
+
<path d="M12 22s8-6 8-12a8 8 0 1 0-16 0c0 6 8 12 8 12z"></path>
|
|
933
|
+
</svg>
|
|
934
|
+
<svg v-else class="pin-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6">
|
|
935
|
+
<path d="M12 22s8-6 8-12a8 8 0 1 0-16 0c0 6 8 12 8 12z"></path>
|
|
936
|
+
</svg>
|
|
937
|
+
</button>
|
|
891
938
|
<button
|
|
892
939
|
v-if="isResumeCommandAvailable(session)"
|
|
893
940
|
class="session-item-copy"
|
|
@@ -908,6 +955,7 @@
|
|
|
908
955
|
</div>
|
|
909
956
|
</div>
|
|
910
957
|
</div>
|
|
958
|
+
<div v-else-if="!sessionStandalone" class="session-list session-list-placeholder"></div>
|
|
911
959
|
|
|
912
960
|
<div :class="['session-preview', { active: !!activeSession }]" :ref="setSessionPreviewContainerRef">
|
|
913
961
|
<template v-if="activeSession">
|
|
@@ -932,7 +980,7 @@
|
|
|
932
980
|
class="btn-session-delete"
|
|
933
981
|
@click="deleteSession(activeSession)"
|
|
934
982
|
:disabled="!activeSession || sessionsLoading || sessionDeleting[getSessionExportKey(activeSession)]">
|
|
935
|
-
{{ (activeSession && sessionDeleting[getSessionExportKey(activeSession)]) ? '
|
|
983
|
+
{{ (activeSession && sessionDeleting[getSessionExportKey(activeSession)]) ? '移入中...' : '移入回收站' }}
|
|
936
984
|
</button>
|
|
937
985
|
<button
|
|
938
986
|
class="btn-session-export"
|
|
@@ -950,7 +998,7 @@
|
|
|
950
998
|
</div>
|
|
951
999
|
</div>
|
|
952
1000
|
|
|
953
|
-
<div v-if="sessionDetailLoading" class="session-preview-empty">
|
|
1001
|
+
<div v-if="sessionDetailLoading && !sessionPreviewLoadingMore" class="session-preview-empty">
|
|
954
1002
|
正在加载会话内容...
|
|
955
1003
|
</div>
|
|
956
1004
|
|
|
@@ -962,16 +1010,42 @@
|
|
|
962
1010
|
当前会话暂无可展示消息
|
|
963
1011
|
</div>
|
|
964
1012
|
|
|
1013
|
+
<div v-else-if="sessionPreviewRenderEnabled && !activeSessionVisibleMessages.length" class="session-preview-empty">
|
|
1014
|
+
<span>正在渲染会话内容...</span>
|
|
1015
|
+
<button class="btn-session-refresh" @click="primeSessionPreviewMessageRender" :disabled="sessionDetailLoading">
|
|
1016
|
+
重新渲染
|
|
1017
|
+
</button>
|
|
1018
|
+
</div>
|
|
1019
|
+
|
|
1020
|
+
<div v-else-if="!sessionPreviewRenderEnabled" class="session-preview-empty">
|
|
1021
|
+
正在准备会话内容...
|
|
1022
|
+
</div>
|
|
1023
|
+
|
|
965
1024
|
<div v-else class="session-preview-body">
|
|
966
1025
|
<div class="session-preview-messages">
|
|
967
1026
|
<div v-if="activeSessionDetailClipped" class="session-item-sub session-item-wrap">
|
|
968
1027
|
仅展示最近 {{ activeSessionMessages.length }} 条消息。
|
|
969
1028
|
</div>
|
|
970
1029
|
<div
|
|
971
|
-
v-
|
|
1030
|
+
v-if="canLoadMoreSessionMessages"
|
|
1031
|
+
class="session-item-sub session-item-wrap"
|
|
1032
|
+
style="display:flex;align-items:center;justify-content:space-between;gap:8px;">
|
|
1033
|
+
<span>已显示 {{ activeSessionVisibleMessages.length }} / {{ activeSessionMessages.length }} 条</span>
|
|
1034
|
+
<button class="btn-session-refresh" @click="loadMoreSessionMessages()" :disabled="sessionDetailLoading || sessionPreviewLoadingMore">
|
|
1035
|
+
{{ sessionPreviewLoadingMore ? '加载中...' : ('加载更多(剩余 ' + sessionPreviewRemainingCount + ')') }}
|
|
1036
|
+
</button>
|
|
1037
|
+
</div>
|
|
1038
|
+
<div
|
|
1039
|
+
v-if="sessionPreviewLoadingMore"
|
|
1040
|
+
class="session-item-sub session-item-wrap">
|
|
1041
|
+
正在加载更早消息...
|
|
1042
|
+
</div>
|
|
1043
|
+
<div
|
|
1044
|
+
v-for="(msg, idx) in activeSessionVisibleMessages"
|
|
972
1045
|
:key="getRecordRenderKey(msg, idx)"
|
|
1046
|
+
v-memo="[msg.text, msg.timestamp, msg.roleLabel, msg.normalizedRole]"
|
|
973
1047
|
:data-message-key="getRecordRenderKey(msg, idx)"
|
|
974
|
-
:ref="(
|
|
1048
|
+
:ref="getSessionMessageRefBinder(getRecordRenderKey(msg, idx))"
|
|
975
1049
|
:class="['session-msg', msg.normalizedRole === 'user' ? 'user' : (msg.normalizedRole === 'system' ? 'system' : 'assistant')]">
|
|
976
1050
|
<div class="session-msg-header">
|
|
977
1051
|
<div class="session-msg-meta">
|
|
@@ -984,11 +1058,12 @@
|
|
|
984
1058
|
</div>
|
|
985
1059
|
</div>
|
|
986
1060
|
</div>
|
|
987
|
-
<aside v-if="sessionTimelineNodes.length" class="session-timeline" aria-label="会话时间轴">
|
|
1061
|
+
<aside v-if="sessionPreviewRenderEnabled && sessionTimelineNodes.length" class="session-timeline" aria-label="会话时间轴">
|
|
988
1062
|
<div class="session-timeline-track"></div>
|
|
989
1063
|
<button
|
|
990
1064
|
v-for="node in sessionTimelineNodes"
|
|
991
1065
|
:key="'timeline-' + node.key"
|
|
1066
|
+
v-memo="[sessionTimelineActiveKey === node.key, node.safePercent, node.title]"
|
|
992
1067
|
type="button"
|
|
993
1068
|
:class="['session-timeline-node', { active: sessionTimelineActiveKey === node.key }]"
|
|
994
1069
|
:aria-current="sessionTimelineActiveKey === node.key ? 'true' : null"
|
|
@@ -1019,39 +1094,140 @@
|
|
|
1019
1094
|
id="panel-settings"
|
|
1020
1095
|
role="tabpanel"
|
|
1021
1096
|
:aria-labelledby="'tab-settings'">
|
|
1022
|
-
<div class="
|
|
1023
|
-
<
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1097
|
+
<div class="config-subtabs settings-subtabs" role="tablist" aria-label="设置子标签">
|
|
1098
|
+
<button
|
|
1099
|
+
id="settings-tab-backup"
|
|
1100
|
+
role="tab"
|
|
1101
|
+
aria-controls="settings-panel-backup"
|
|
1102
|
+
:aria-selected="settingsTab === 'backup'"
|
|
1103
|
+
tabindex="0"
|
|
1104
|
+
:class="['config-subtab', { active: settingsTab === 'backup' }]"
|
|
1105
|
+
@click="onSettingsTabClick('backup')">
|
|
1106
|
+
备份与导入
|
|
1028
1107
|
</button>
|
|
1029
|
-
<button
|
|
1030
|
-
|
|
1108
|
+
<button
|
|
1109
|
+
id="settings-tab-trash"
|
|
1110
|
+
role="tab"
|
|
1111
|
+
aria-controls="settings-panel-trash"
|
|
1112
|
+
:aria-selected="settingsTab === 'trash'"
|
|
1113
|
+
tabindex="0"
|
|
1114
|
+
:class="['config-subtab', { active: settingsTab === 'trash' }]"
|
|
1115
|
+
@click="onSettingsTabClick('trash')">
|
|
1116
|
+
回收站
|
|
1117
|
+
<span class="settings-tab-badge">{{ sessionTrashCount }}</span>
|
|
1031
1118
|
</button>
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1119
|
+
</div>
|
|
1120
|
+
|
|
1121
|
+
<div
|
|
1122
|
+
v-show="settingsTab === 'backup'"
|
|
1123
|
+
id="settings-panel-backup"
|
|
1124
|
+
role="tabpanel"
|
|
1125
|
+
aria-labelledby="settings-tab-backup">
|
|
1126
|
+
<div class="selector-section">
|
|
1127
|
+
<div class="selector-header">
|
|
1128
|
+
<span class="selector-title">Claude 配置</span>
|
|
1129
|
+
</div>
|
|
1130
|
+
<button class="btn-tool" @click="downloadClaudeDirectory" :disabled="claudeDownloadLoading">
|
|
1131
|
+
{{ claudeDownloadLoading ? ('备份中 ' + claudeDownloadProgress + '%') : '一键备份 ~/.claude' }}
|
|
1132
|
+
</button>
|
|
1133
|
+
<button class="btn-tool" @click="triggerClaudeImport" :disabled="claudeImportLoading">
|
|
1134
|
+
{{ claudeImportLoading ? '导入中...' : '导入 ~/.claude 备份' }}
|
|
1135
|
+
</button>
|
|
1136
|
+
<input
|
|
1137
|
+
ref="claudeImportInput"
|
|
1138
|
+
class="sr-only"
|
|
1139
|
+
type="file"
|
|
1140
|
+
accept=".zip"
|
|
1141
|
+
@change="handleClaudeImportChange">
|
|
1142
|
+
</div>
|
|
1143
|
+
<div class="selector-section">
|
|
1144
|
+
<div class="selector-header">
|
|
1145
|
+
<span class="selector-title">Codex 配置</span>
|
|
1146
|
+
</div>
|
|
1147
|
+
<button class="btn-tool" @click="downloadCodexDirectory" :disabled="codexDownloadLoading">
|
|
1148
|
+
{{ codexDownloadLoading ? ('备份中 ' + codexDownloadProgress + '%') : '一键备份 ~/.codex' }}
|
|
1149
|
+
</button>
|
|
1150
|
+
<button class="btn-tool" @click="triggerCodexImport" :disabled="codexImportLoading">
|
|
1151
|
+
{{ codexImportLoading ? '导入中...' : '导入 ~/.codex 备份' }}
|
|
1152
|
+
</button>
|
|
1153
|
+
<input
|
|
1154
|
+
ref="codexImportInput"
|
|
1155
|
+
class="sr-only"
|
|
1156
|
+
type="file"
|
|
1157
|
+
accept=".zip"
|
|
1158
|
+
@change="handleCodexImportChange">
|
|
1159
|
+
</div>
|
|
1160
|
+
</div>
|
|
1161
|
+
|
|
1162
|
+
<div
|
|
1163
|
+
v-show="settingsTab === 'trash'"
|
|
1164
|
+
id="settings-panel-trash"
|
|
1165
|
+
role="tabpanel"
|
|
1166
|
+
aria-labelledby="settings-tab-trash">
|
|
1167
|
+
<div class="selector-section">
|
|
1168
|
+
<div class="selector-header settings-tab-header">
|
|
1169
|
+
<div>
|
|
1170
|
+
<span class="selector-title">会话回收站</span>
|
|
1171
|
+
</div>
|
|
1172
|
+
<div class="settings-tab-actions">
|
|
1173
|
+
<button class="btn-tool btn-tool-compact" @click="loadSessionTrash({ forceRefresh: true })" :disabled="sessionTrashLoading || sessionTrashClearing">
|
|
1174
|
+
{{ sessionTrashLoading ? '刷新中...' : '刷新列表' }}
|
|
1175
|
+
</button>
|
|
1176
|
+
<button class="btn-tool btn-tool-compact" @click="clearSessionTrash" :disabled="sessionTrashClearing || sessionTrashLoading || !(Number(sessionTrashCount) > 0)">
|
|
1177
|
+
{{ sessionTrashClearing ? '清空中...' : '清空回收站' }}
|
|
1178
|
+
</button>
|
|
1179
|
+
</div>
|
|
1180
|
+
</div>
|
|
1181
|
+
|
|
1182
|
+
<div v-if="getSessionTrashViewState() === 'loading'" class="session-empty">
|
|
1183
|
+
正在加载回收站...
|
|
1184
|
+
</div>
|
|
1185
|
+
<div v-else-if="getSessionTrashViewState() === 'empty'" class="session-empty">
|
|
1186
|
+
回收站为空
|
|
1187
|
+
</div>
|
|
1188
|
+
<div v-else-if="getSessionTrashViewState() === 'retry'" class="session-empty">
|
|
1189
|
+
回收站列表加载失败,请刷新重试
|
|
1190
|
+
</div>
|
|
1191
|
+
<div v-else class="trash-list">
|
|
1192
|
+
<div v-for="item in visibleSessionTrashItems" :key="item.trashId" class="trash-item session-item session-card">
|
|
1193
|
+
<div class="trash-item-header session-item-header">
|
|
1194
|
+
<div class="trash-item-main">
|
|
1195
|
+
<div class="trash-item-mainline">
|
|
1196
|
+
<div class="trash-item-title">{{ item.title || item.sessionId }}</div>
|
|
1197
|
+
<span class="session-count-badge">{{ item.messageCount ?? 0 }}</span>
|
|
1198
|
+
</div>
|
|
1199
|
+
<div class="trash-item-meta session-item-meta">
|
|
1200
|
+
<span class="session-source">{{ item.sourceLabel }}</span>
|
|
1201
|
+
</div>
|
|
1202
|
+
</div>
|
|
1203
|
+
<div class="trash-item-side">
|
|
1204
|
+
<div class="trash-item-actions session-item-actions">
|
|
1205
|
+
<button class="btn-mini" @click="restoreSessionTrash(item)" :disabled="sessionTrashLoading || sessionTrashClearing || isSessionTrashActionBusy(item)">
|
|
1206
|
+
{{ sessionTrashRestoring[getSessionTrashActionKey(item)] ? '恢复中...' : '恢复' }}
|
|
1207
|
+
</button>
|
|
1208
|
+
<button class="btn-mini delete" @click="purgeSessionTrash(item)" :disabled="sessionTrashLoading || sessionTrashClearing || isSessionTrashActionBusy(item)">
|
|
1209
|
+
{{ sessionTrashPurging[getSessionTrashActionKey(item)] ? '删除中...' : '彻底删除' }}
|
|
1210
|
+
</button>
|
|
1211
|
+
</div>
|
|
1212
|
+
<div class="trash-item-time session-item-time">{{ item.deletedAt || item.updatedAt || 'unknown time' }}</div>
|
|
1213
|
+
</div>
|
|
1214
|
+
</div>
|
|
1215
|
+
<div v-if="item.cwd" class="trash-item-path session-item-sub session-item-wrap">
|
|
1216
|
+
<span class="trash-item-label">工作区</span>
|
|
1217
|
+
<span>{{ item.cwd }}</span>
|
|
1218
|
+
</div>
|
|
1219
|
+
<div class="trash-item-path session-item-sub session-item-wrap">
|
|
1220
|
+
<span class="trash-item-label">原文件</span>
|
|
1221
|
+
<span>{{ item.originalFilePath }}</span>
|
|
1222
|
+
</div>
|
|
1223
|
+
</div>
|
|
1224
|
+
<div v-if="sessionTrashHasMoreItems" class="trash-list-footer">
|
|
1225
|
+
<button class="btn-tool btn-tool-compact" @click="loadMoreSessionTrashItems" :disabled="sessionTrashLoading || sessionTrashClearing">
|
|
1226
|
+
加载更多(剩余 {{ sessionTrashHiddenCount }} 项)
|
|
1227
|
+
</button>
|
|
1228
|
+
</div>
|
|
1229
|
+
</div>
|
|
1042
1230
|
</div>
|
|
1043
|
-
<button class="btn-tool" @click="downloadCodexDirectory" :disabled="codexDownloadLoading">
|
|
1044
|
-
{{ codexDownloadLoading ? ('备份中 ' + codexDownloadProgress + '%') : '一键备份 ~/.codex' }}
|
|
1045
|
-
</button>
|
|
1046
|
-
<button class="btn-tool" @click="triggerCodexImport" :disabled="codexImportLoading">
|
|
1047
|
-
{{ codexImportLoading ? '导入中...' : '导入 ~/.codex 备份' }}
|
|
1048
|
-
</button>
|
|
1049
|
-
<input
|
|
1050
|
-
ref="codexImportInput"
|
|
1051
|
-
class="sr-only"
|
|
1052
|
-
type="file"
|
|
1053
|
-
accept=".zip"
|
|
1054
|
-
@change="handleCodexImportChange">
|
|
1055
1231
|
</div>
|
|
1056
1232
|
</div>
|
|
1057
1233
|
|
|
@@ -1655,24 +1831,70 @@
|
|
|
1655
1831
|
</div>
|
|
1656
1832
|
</div>
|
|
1657
1833
|
|
|
1834
|
+
|
|
1658
1835
|
<div class="form-group">
|
|
1659
1836
|
<label class="form-label">AGENTS.md 内容</label>
|
|
1837
|
+
<div
|
|
1838
|
+
v-if="!agentsLoading && (hasAgentsContentChanged() || agentsDiffVisible)"
|
|
1839
|
+
class="agents-diff-save-alert">
|
|
1840
|
+
{{ agentsDiffVisible ? '预览模式:当前改动尚未保存,只有点击“应用”后才会写入文件。' : '检测到未保存改动:关闭页面或应用前请先保存。' }}
|
|
1841
|
+
</div>
|
|
1842
|
+
<div v-if="agentsDiffVisible">
|
|
1843
|
+
<div
|
|
1844
|
+
v-if="!agentsDiffLoading && !agentsDiffError && !agentsDiffTruncated && (agentsDiffStats.added || agentsDiffStats.removed)"
|
|
1845
|
+
class="agents-diff-summary">
|
|
1846
|
+
<span class="agents-diff-stat add">+{{ agentsDiffStats.added }}</span>
|
|
1847
|
+
<span class="agents-diff-stat del">-{{ agentsDiffStats.removed }}</span>
|
|
1848
|
+
</div>
|
|
1849
|
+
<div v-if="agentsDiffLoading" class="state-message">生成差异中...</div>
|
|
1850
|
+
<div v-else-if="agentsDiffError" class="state-message error">{{ agentsDiffError }}</div>
|
|
1851
|
+
<div v-else-if="agentsDiffTruncated" class="agents-diff-empty">内容过大,已跳过逐行差异预览</div>
|
|
1852
|
+
<div v-else-if="!agentsDiffHasChanges" class="agents-diff-empty">未检测到改动</div>
|
|
1853
|
+
<div v-else class="agents-diff-view agents-diff-editor">
|
|
1854
|
+
<div
|
|
1855
|
+
v-for="(line, index) in agentsDiffLines"
|
|
1856
|
+
:key="line.key || (line.type + '-' + index)"
|
|
1857
|
+
:class="['agents-diff-line', line.type]">
|
|
1858
|
+
<span class="agents-diff-line-sign">
|
|
1859
|
+
{{ line.type === 'add' ? '+' : (line.type === 'del' ? '-' : ' ') }}
|
|
1860
|
+
</span>
|
|
1861
|
+
<span class="agents-diff-line-text">{{ line.value }}</span>
|
|
1862
|
+
</div>
|
|
1863
|
+
</div>
|
|
1864
|
+
</div>
|
|
1660
1865
|
<textarea
|
|
1866
|
+
v-else
|
|
1661
1867
|
v-model="agentsContent"
|
|
1662
1868
|
class="form-input template-editor"
|
|
1663
1869
|
spellcheck="false"
|
|
1664
1870
|
:readonly="agentsLoading"
|
|
1871
|
+
@input="onAgentsContentInput"
|
|
1665
1872
|
placeholder="在这里编辑 AGENTS.md 内容"></textarea>
|
|
1666
1873
|
<div class="template-editor-warning">
|
|
1667
1874
|
{{ agentsModalHint }}
|
|
1875
|
+
<div class="agents-diff-hint">快捷键:Esc(差异预览时返回编辑,编辑时关闭窗口)。</div>
|
|
1876
|
+
<div v-if="!agentsDiffVisible" class="agents-diff-hint">保存需两步:先点击“确认”预览差异,再点击“应用”保存。</div>
|
|
1877
|
+
<div v-else-if="agentsDiffLoading || agentsSaving" class="agents-diff-hint">正在生成差异或应用中,操作暂不可用。</div>
|
|
1878
|
+
<div v-else-if="agentsDiffError" class="agents-diff-hint">差异预览失败,请返回编辑后重试。</div>
|
|
1879
|
+
<div v-else-if="!agentsDiffHasChanges" class="agents-diff-hint">未检测到改动,可返回编辑继续修改或取消退出。</div>
|
|
1880
|
+
<div v-else-if="agentsDiffTruncated" class="agents-diff-hint">内容过大,已跳过预览,可点击“应用”保存或“返回编辑”继续修改。</div>
|
|
1881
|
+
<div v-else class="agents-diff-hint">当前为预览模式,可点击“应用”保存或“返回编辑”继续修改。</div>
|
|
1668
1882
|
</div>
|
|
1669
1883
|
</div>
|
|
1884
|
+
|
|
1670
1885
|
</div>
|
|
1671
1886
|
|
|
1672
1887
|
<div class="btn-group modal-editor-footer">
|
|
1673
|
-
<button class="btn btn-cancel" @click="closeAgentsModal">取消</button>
|
|
1674
|
-
<button
|
|
1675
|
-
|
|
1888
|
+
<button class="btn btn-cancel" @click="closeAgentsModal" :disabled="agentsSaving || agentsDiffLoading">取消</button>
|
|
1889
|
+
<button
|
|
1890
|
+
v-if="agentsDiffVisible"
|
|
1891
|
+
class="btn"
|
|
1892
|
+
@click="resetAgentsDiffState"
|
|
1893
|
+
:disabled="agentsSaving || agentsDiffLoading">
|
|
1894
|
+
返回编辑
|
|
1895
|
+
</button>
|
|
1896
|
+
<button class="btn btn-confirm" @click="applyAgentsContent" :disabled="agentsSaving || agentsLoading || agentsDiffLoading || (agentsDiffVisible && !agentsDiffHasChanges)">
|
|
1897
|
+
{{ agentsSaving ? (agentsDiffVisible ? '应用中...' : '确认中...') : (agentsDiffVisible ? '应用' : '确认') }}
|
|
1676
1898
|
</button>
|
|
1677
1899
|
</div>
|
|
1678
1900
|
</div>
|
|
@@ -1840,6 +2062,27 @@
|
|
|
1840
2062
|
</div>
|
|
1841
2063
|
</div>
|
|
1842
2064
|
|
|
2065
|
+
<div v-if="showConfirmDialog" class="modal-overlay" @click.self="closeConfirmDialog">
|
|
2066
|
+
<div
|
|
2067
|
+
class="modal confirm-dialog"
|
|
2068
|
+
role="alertdialog"
|
|
2069
|
+
aria-modal="true"
|
|
2070
|
+
aria-describedby="confirm-dialog-message"
|
|
2071
|
+
:aria-labelledby="confirmDialogTitle ? 'confirm-dialog-title' : null"
|
|
2072
|
+
:aria-label="confirmDialogTitle ? null : '确认操作'">
|
|
2073
|
+
<div id="confirm-dialog-title" class="modal-title">{{ confirmDialogTitle }}</div>
|
|
2074
|
+
<div id="confirm-dialog-message" class="confirm-dialog-message">{{ confirmDialogMessage }}</div>
|
|
2075
|
+
<div class="btn-group confirm-dialog-actions">
|
|
2076
|
+
<button class="btn btn-cancel" @click="closeConfirmDialog">{{ confirmDialogCancelText }}</button>
|
|
2077
|
+
<button
|
|
2078
|
+
:class="['btn', 'btn-confirm', confirmDialogDanger ? 'btn-danger' : '']"
|
|
2079
|
+
@click="resolveConfirmDialog(true)">
|
|
2080
|
+
{{ confirmDialogConfirmText }}
|
|
2081
|
+
</button>
|
|
2082
|
+
</div>
|
|
2083
|
+
</div>
|
|
2084
|
+
</div>
|
|
2085
|
+
|
|
1843
2086
|
<!-- Toast通知 -->
|
|
1844
2087
|
|
|
1845
2088
|
<!-- Toast -->
|