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/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: mainTab === 'config' && configMode === 'codex' }"
64
- @click="switchConfigMode('codex')">Codex 配置</button>
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: mainTab === 'config' && configMode === 'claude' }"
73
- @click="switchConfigMode('claude')">Claude Code 配置</button>
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: mainTab === 'config' && configMode === 'openclaw' }"
82
- @click="switchConfigMode('openclaw')">OpenClaw 配置</button>
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: mainTab === 'sessions' }"
91
- @click="switchMainTab('sessions')">会话浏览</button>
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: mainTab === 'settings' }"
100
- @click="switchMainTab('settings')">设置</button>
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
- :aria-pressed="mainTab === 'config' && configMode === 'codex'"
144
- :class="['side-item', { active: mainTab === 'config' && configMode === 'codex' }]"
145
- @click="switchConfigMode('codex')">
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
- :aria-pressed="mainTab === 'config' && configMode === 'claude'"
159
- :class="['side-item', { active: mainTab === 'config' && configMode === 'claude' }]"
160
- @click="switchConfigMode('claude')">
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
- :aria-pressed="mainTab === 'config' && configMode === 'openclaw'"
174
- :class="['side-item', { active: mainTab === 'config' && configMode === 'openclaw' }]"
175
- @click="switchConfigMode('openclaw')">
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
- :aria-pressed="mainTab === 'sessions'"
193
- :class="['side-item', { active: mainTab === 'sessions' }]"
194
- @click="switchMainTab('sessions')">
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
- :aria-pressed="mainTab === 'settings'"
212
- :class="['side-item', { active: mainTab === 'settings' }]"
213
- @click="switchMainTab('settings')">
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="switchConfigMode('codex')">
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="switchConfigMode('claude')">
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="switchConfigMode('openclaw')">
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], disabled: !shouldAllowProviderShare(provider) }"
559
- :disabled="!shouldAllowProviderShare(provider)"
588
+ :class="{ loading: providerShareLoading[provider.name] }"
589
+ disabled
560
590
  @click="copyProviderShareCommand(provider)"
561
- :title="shouldAllowProviderShare(provider) ? '分享导入命令' : '本地入口不可分享'">
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="分享导入命令" aria-label="Share import command">
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 sessionsList"
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: activeSession && getSessionExportKey(activeSession) === getSessionExportKey(session)
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-for="(msg, idx) in activeSessionMessages"
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="(el) => bindSessionMessageRef(getRecordRenderKey(msg, idx), el)"
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="selector-section">
1023
- <div class="selector-header">
1024
- <span class="selector-title">Claude 配置</span>
1025
- </div>
1026
- <button class="btn-tool" @click="downloadClaudeDirectory" :disabled="claudeDownloadLoading">
1027
- {{ claudeDownloadLoading ? ('备份中 ' + claudeDownloadProgress + '%') : '一键备份 ~/.claude' }}
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 class="btn-tool" @click="triggerClaudeImport" :disabled="claudeImportLoading">
1030
- {{ claudeImportLoading ? '导入中...' : '导入 ~/.claude 备份' }}
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
- <input
1033
- ref="claudeImportInput"
1034
- class="sr-only"
1035
- type="file"
1036
- accept=".zip"
1037
- @change="handleClaudeImportChange">
1038
- </div>
1039
- <div class="selector-section">
1040
- <div class="selector-header">
1041
- <span class="selector-title">Codex 配置</span>
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 class="btn btn-confirm" @click="applyAgentsContent" :disabled="agentsSaving || agentsLoading">
1675
- {{ agentsSaving ? '保存中...' : '保存' }}
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 -->