codexmate 0.0.17 → 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>
@@ -298,13 +314,13 @@
298
314
  </div>
299
315
 
300
316
  <div v-if="false && mainTab === 'config' && !sessionStandalone" class="config-subtabs">
301
- <button :class="['config-subtab', { active: configMode === 'codex' }]" @click="switchConfigMode('codex')">
317
+ <button :class="['config-subtab', { active: configMode === 'codex' }]" @click="onConfigTabClick('codex', $event)">
302
318
  Codex 配置
303
319
  </button>
304
- <button :class="['config-subtab', { active: configMode === 'claude' }]" @click="switchConfigMode('claude')">
320
+ <button :class="['config-subtab', { active: configMode === 'claude' }]" @click="onConfigTabClick('claude', $event)">
305
321
  Claude Code 配置
306
322
  </button>
307
- <button :class="['config-subtab', { active: configMode === 'openclaw' }]" @click="switchConfigMode('openclaw')">
323
+ <button :class="['config-subtab', { active: configMode === 'openclaw' }]" @click="onConfigTabClick('openclaw', $event)">
308
324
  OpenClaw 配置
309
325
  </button>
310
326
  </div>
@@ -569,10 +585,11 @@
569
585
  <button
570
586
  v-if="!provider.readOnly"
571
587
  class="card-action-btn"
572
- :class="{ loading: providerShareLoading[provider.name], disabled: !shouldAllowProviderShare(provider) }"
573
- :disabled="!shouldAllowProviderShare(provider)"
588
+ :class="{ loading: providerShareLoading[provider.name] }"
589
+ disabled
574
590
  @click="copyProviderShareCommand(provider)"
575
- :title="shouldAllowProviderShare(provider) ? '分享导入命令' : '本地入口不可分享'">
591
+ title="分享导入命令(暂时禁用)"
592
+ aria-label="Share import command">
576
593
  <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
577
594
  <path d="M4 12v7a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-7"/>
578
595
  <path d="M16 6l-4-4-4 4"/>
@@ -885,14 +902,16 @@
885
902
  </div>
886
903
 
887
904
  <div v-else :class="['session-layout', { 'session-standalone': sessionStandalone }]">
888
- <div v-if="!sessionStandalone" class="session-list">
905
+ <div v-if="!sessionStandalone && sessionListRenderEnabled" class="session-list">
889
906
  <div
890
- v-for="session in sessionsList"
907
+ v-for="session in sortedSessionsList"
891
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]"
892
910
  :class="[
893
911
  'session-item',
894
912
  {
895
- active: activeSession && getSessionExportKey(activeSession) === getSessionExportKey(session)
913
+ active: activeSessionExportKey === getSessionExportKey(session),
914
+ pinned: isSessionPinned(session)
896
915
  }
897
916
  ]"
898
917
  @click="selectSession(session)">
@@ -902,6 +921,20 @@
902
921
  <span class="session-count-badge">{{ session.messageCount ?? 0 }}</span>
903
922
  </div>
904
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>
905
938
  <button
906
939
  v-if="isResumeCommandAvailable(session)"
907
940
  class="session-item-copy"
@@ -922,6 +955,7 @@
922
955
  </div>
923
956
  </div>
924
957
  </div>
958
+ <div v-else-if="!sessionStandalone" class="session-list session-list-placeholder"></div>
925
959
 
926
960
  <div :class="['session-preview', { active: !!activeSession }]" :ref="setSessionPreviewContainerRef">
927
961
  <template v-if="activeSession">
@@ -946,7 +980,7 @@
946
980
  class="btn-session-delete"
947
981
  @click="deleteSession(activeSession)"
948
982
  :disabled="!activeSession || sessionsLoading || sessionDeleting[getSessionExportKey(activeSession)]">
949
- {{ (activeSession && sessionDeleting[getSessionExportKey(activeSession)]) ? '删除中...' : '删除会话' }}
983
+ {{ (activeSession && sessionDeleting[getSessionExportKey(activeSession)]) ? '移入中...' : '移入回收站' }}
950
984
  </button>
951
985
  <button
952
986
  class="btn-session-export"
@@ -964,7 +998,7 @@
964
998
  </div>
965
999
  </div>
966
1000
 
967
- <div v-if="sessionDetailLoading" class="session-preview-empty">
1001
+ <div v-if="sessionDetailLoading && !sessionPreviewLoadingMore" class="session-preview-empty">
968
1002
  正在加载会话内容...
969
1003
  </div>
970
1004
 
@@ -976,16 +1010,42 @@
976
1010
  当前会话暂无可展示消息
977
1011
  </div>
978
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
+
979
1024
  <div v-else class="session-preview-body">
980
1025
  <div class="session-preview-messages">
981
1026
  <div v-if="activeSessionDetailClipped" class="session-item-sub session-item-wrap">
982
1027
  仅展示最近 {{ activeSessionMessages.length }} 条消息。
983
1028
  </div>
984
1029
  <div
985
- 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"
986
1045
  :key="getRecordRenderKey(msg, idx)"
1046
+ v-memo="[msg.text, msg.timestamp, msg.roleLabel, msg.normalizedRole]"
987
1047
  :data-message-key="getRecordRenderKey(msg, idx)"
988
- :ref="(el) => bindSessionMessageRef(getRecordRenderKey(msg, idx), el)"
1048
+ :ref="getSessionMessageRefBinder(getRecordRenderKey(msg, idx))"
989
1049
  :class="['session-msg', msg.normalizedRole === 'user' ? 'user' : (msg.normalizedRole === 'system' ? 'system' : 'assistant')]">
990
1050
  <div class="session-msg-header">
991
1051
  <div class="session-msg-meta">
@@ -998,11 +1058,12 @@
998
1058
  </div>
999
1059
  </div>
1000
1060
  </div>
1001
- <aside v-if="sessionTimelineNodes.length" class="session-timeline" aria-label="会话时间轴">
1061
+ <aside v-if="sessionPreviewRenderEnabled && sessionTimelineNodes.length" class="session-timeline" aria-label="会话时间轴">
1002
1062
  <div class="session-timeline-track"></div>
1003
1063
  <button
1004
1064
  v-for="node in sessionTimelineNodes"
1005
1065
  :key="'timeline-' + node.key"
1066
+ v-memo="[sessionTimelineActiveKey === node.key, node.safePercent, node.title]"
1006
1067
  type="button"
1007
1068
  :class="['session-timeline-node', { active: sessionTimelineActiveKey === node.key }]"
1008
1069
  :aria-current="sessionTimelineActiveKey === node.key ? 'true' : null"
@@ -1033,39 +1094,140 @@
1033
1094
  id="panel-settings"
1034
1095
  role="tabpanel"
1035
1096
  :aria-labelledby="'tab-settings'">
1036
- <div class="selector-section">
1037
- <div class="selector-header">
1038
- <span class="selector-title">Claude 配置</span>
1039
- </div>
1040
- <button class="btn-tool" @click="downloadClaudeDirectory" :disabled="claudeDownloadLoading">
1041
- {{ 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
+ 备份与导入
1042
1107
  </button>
1043
- <button class="btn-tool" @click="triggerClaudeImport" :disabled="claudeImportLoading">
1044
- {{ 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>
1045
1118
  </button>
1046
- <input
1047
- ref="claudeImportInput"
1048
- class="sr-only"
1049
- type="file"
1050
- accept=".zip"
1051
- @change="handleClaudeImportChange">
1052
- </div>
1053
- <div class="selector-section">
1054
- <div class="selector-header">
1055
- <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>
1056
1230
  </div>
1057
- <button class="btn-tool" @click="downloadCodexDirectory" :disabled="codexDownloadLoading">
1058
- {{ codexDownloadLoading ? ('备份中 ' + codexDownloadProgress + '%') : '一键备份 ~/.codex' }}
1059
- </button>
1060
- <button class="btn-tool" @click="triggerCodexImport" :disabled="codexImportLoading">
1061
- {{ codexImportLoading ? '导入中...' : '导入 ~/.codex 备份' }}
1062
- </button>
1063
- <input
1064
- ref="codexImportInput"
1065
- class="sr-only"
1066
- type="file"
1067
- accept=".zip"
1068
- @change="handleCodexImportChange">
1069
1231
  </div>
1070
1232
  </div>
1071
1233
 
@@ -1669,24 +1831,70 @@
1669
1831
  </div>
1670
1832
  </div>
1671
1833
 
1834
+
1672
1835
  <div class="form-group">
1673
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>
1674
1865
  <textarea
1866
+ v-else
1675
1867
  v-model="agentsContent"
1676
1868
  class="form-input template-editor"
1677
1869
  spellcheck="false"
1678
1870
  :readonly="agentsLoading"
1871
+ @input="onAgentsContentInput"
1679
1872
  placeholder="在这里编辑 AGENTS.md 内容"></textarea>
1680
1873
  <div class="template-editor-warning">
1681
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>
1682
1882
  </div>
1683
1883
  </div>
1884
+
1684
1885
  </div>
1685
1886
 
1686
1887
  <div class="btn-group modal-editor-footer">
1687
- <button class="btn btn-cancel" @click="closeAgentsModal">取消</button>
1688
- <button class="btn btn-confirm" @click="applyAgentsContent" :disabled="agentsSaving || agentsLoading">
1689
- {{ 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 ? '应用' : '确认') }}
1690
1898
  </button>
1691
1899
  </div>
1692
1900
  </div>
@@ -1854,6 +2062,27 @@
1854
2062
  </div>
1855
2063
  </div>
1856
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
+
1857
2086
  <!-- Toast通知 -->
1858
2087
 
1859
2088
  <!-- Toast -->