codexmate 0.0.19 → 0.0.21

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.
Files changed (102) hide show
  1. package/README.en.md +349 -255
  2. package/README.md +284 -248
  3. package/cli/agents-files.js +162 -0
  4. package/cli/archive-helpers.js +446 -0
  5. package/cli/auth-profiles.js +359 -0
  6. package/cli/builtin-proxy.js +580 -0
  7. package/cli/claude-proxy.js +998 -0
  8. package/cli/config-bootstrap.js +384 -0
  9. package/cli/config-health.js +338 -0
  10. package/cli/openclaw-config.js +629 -0
  11. package/cli/skills.js +1141 -0
  12. package/cli/zip-commands.js +510 -0
  13. package/cli.js +13129 -12973
  14. package/lib/cli-file-utils.js +151 -151
  15. package/lib/cli-models-utils.js +419 -152
  16. package/lib/cli-network-utils.js +164 -148
  17. package/lib/cli-path-utils.js +69 -0
  18. package/lib/cli-session-utils.js +121 -121
  19. package/lib/cli-sessions.js +386 -0
  20. package/lib/cli-utils.js +155 -155
  21. package/lib/download-artifacts.js +77 -0
  22. package/lib/mcp-stdio.js +440 -440
  23. package/lib/task-orchestrator.js +869 -0
  24. package/lib/text-diff.js +303 -303
  25. package/lib/workflow-engine.js +340 -340
  26. package/package.json +74 -63
  27. package/res/json5.min.js +1 -1
  28. package/res/vue.global.prod.js +13 -0
  29. package/web-ui/app.js +530 -5548
  30. package/web-ui/index.html +33 -2246
  31. package/web-ui/logic.agents-diff.mjs +386 -0
  32. package/web-ui/logic.claude.mjs +168 -0
  33. package/web-ui/logic.mjs +5 -793
  34. package/web-ui/logic.runtime.mjs +124 -0
  35. package/web-ui/logic.sessions.mjs +581 -0
  36. package/web-ui/modules/api.mjs +90 -0
  37. package/web-ui/modules/app.computed.dashboard.mjs +113 -0
  38. package/web-ui/modules/app.computed.index.mjs +15 -0
  39. package/web-ui/modules/app.computed.main-tabs.mjs +195 -0
  40. package/web-ui/modules/app.computed.session.mjs +507 -0
  41. package/web-ui/modules/app.constants.mjs +15 -0
  42. package/web-ui/modules/app.methods.agents.mjs +493 -0
  43. package/web-ui/modules/app.methods.claude-config.mjs +174 -0
  44. package/web-ui/modules/app.methods.codex-config.mjs +640 -0
  45. package/web-ui/modules/app.methods.index.mjs +88 -0
  46. package/web-ui/modules/app.methods.install.mjs +149 -0
  47. package/web-ui/modules/app.methods.navigation.mjs +619 -0
  48. package/web-ui/modules/app.methods.openclaw-core.mjs +814 -0
  49. package/web-ui/modules/app.methods.openclaw-editing.mjs +372 -0
  50. package/web-ui/modules/app.methods.openclaw-persist.mjs +369 -0
  51. package/web-ui/modules/app.methods.providers.mjs +363 -0
  52. package/web-ui/modules/app.methods.runtime.mjs +323 -0
  53. package/web-ui/modules/app.methods.session-actions.mjs +520 -0
  54. package/web-ui/modules/app.methods.session-browser.mjs +626 -0
  55. package/web-ui/modules/app.methods.session-timeline.mjs +448 -0
  56. package/web-ui/modules/app.methods.session-trash.mjs +422 -0
  57. package/web-ui/modules/app.methods.startup-claude.mjs +412 -0
  58. package/web-ui/modules/app.methods.task-orchestration.mjs +471 -0
  59. package/web-ui/modules/config-mode.computed.mjs +126 -124
  60. package/web-ui/modules/skills.computed.mjs +107 -107
  61. package/web-ui/modules/skills.methods.mjs +481 -481
  62. package/web-ui/partials/index/layout-footer.html +13 -0
  63. package/web-ui/partials/index/layout-header.html +402 -0
  64. package/web-ui/partials/index/modal-config-template-agents.html +125 -0
  65. package/web-ui/partials/index/modal-confirm-toast.html +32 -0
  66. package/web-ui/partials/index/modal-health-check.html +72 -0
  67. package/web-ui/partials/index/modal-openclaw-config.html +280 -0
  68. package/web-ui/partials/index/modal-skills.html +184 -0
  69. package/web-ui/partials/index/modals-basic.html +156 -0
  70. package/web-ui/partials/index/panel-config-claude.html +126 -0
  71. package/web-ui/partials/index/panel-config-codex.html +237 -0
  72. package/web-ui/partials/index/panel-config-openclaw.html +78 -0
  73. package/web-ui/partials/index/panel-docs.html +130 -0
  74. package/web-ui/partials/index/panel-market.html +174 -0
  75. package/web-ui/partials/index/panel-orchestration.html +397 -0
  76. package/web-ui/partials/index/panel-sessions.html +292 -0
  77. package/web-ui/partials/index/panel-settings.html +190 -0
  78. package/web-ui/partials/index/panel-usage.html +213 -0
  79. package/web-ui/session-helpers.mjs +559 -362
  80. package/web-ui/source-bundle.cjs +233 -0
  81. package/web-ui/styles/base-theme.css +271 -0
  82. package/web-ui/styles/controls-forms.css +360 -0
  83. package/web-ui/styles/docs-panel.css +182 -0
  84. package/web-ui/styles/feedback.css +108 -0
  85. package/web-ui/styles/health-check-dialog.css +144 -0
  86. package/web-ui/styles/layout-shell.css +376 -0
  87. package/web-ui/styles/modals-core.css +464 -0
  88. package/web-ui/styles/navigation-panels.css +348 -0
  89. package/web-ui/styles/openclaw-structured.css +266 -0
  90. package/web-ui/styles/responsive.css +450 -0
  91. package/web-ui/styles/sessions-list.css +400 -0
  92. package/web-ui/styles/sessions-preview.css +411 -0
  93. package/web-ui/styles/sessions-toolbar-trash.css +243 -0
  94. package/web-ui/styles/sessions-usage.css +628 -0
  95. package/web-ui/styles/skills-list.css +296 -0
  96. package/web-ui/styles/skills-market.css +335 -0
  97. package/web-ui/styles/task-orchestration.css +776 -0
  98. package/web-ui/styles/titles-cards.css +408 -0
  99. package/web-ui/styles.css +18 -4668
  100. package/web-ui.html +17 -17
  101. package/res/screenshot.png +0 -0
  102. package/res/vue.global.js +0 -18552
@@ -0,0 +1,292 @@
1
+ <!-- 会话浏览模式 -->
2
+ <div
3
+ v-show="mainTab === 'sessions'"
4
+ class="mode-content"
5
+ id="panel-sessions"
6
+ role="tabpanel"
7
+ :aria-labelledby="'tab-sessions'">
8
+ <div v-if="sessionStandalone" class="session-standalone-page">
9
+ <div v-if="sessionStandaloneLoading" class="state-message">
10
+ 加载中...
11
+ </div>
12
+ <div v-else-if="sessionStandaloneError" class="state-message error">
13
+ {{ sessionStandaloneError }}
14
+ </div>
15
+ <div v-else>
16
+ <div class="session-standalone-title">
17
+ {{ sessionStandaloneTitle }}
18
+ <span v-if="sessionStandaloneSourceLabel"> · {{ sessionStandaloneSourceLabel }}</span>
19
+ </div>
20
+ <pre class="session-standalone-text">{{ sessionStandaloneText }}</pre>
21
+ </div>
22
+ </div>
23
+ <div v-else>
24
+ <div class="selector-section">
25
+ <div class="selector-header">
26
+ <span class="selector-title">会话来源</span>
27
+ <div class="selector-actions">
28
+ <button class="btn-tool btn-tool-compact" @click="loadSessions({ forceRefresh: true })" :disabled="sessionsLoading">
29
+ {{ sessionsLoading ? '刷新中...' : '刷新会话' }}
30
+ </button>
31
+ </div>
32
+ </div>
33
+ <div class="session-toolbar">
34
+ <div class="session-toolbar-group">
35
+ <select class="session-source-select" v-model="sessionFilterSource" @change="onSessionSourceChange" :disabled="sessionsLoading">
36
+ <option value="all">全部</option>
37
+ <option value="codex">Codex</option>
38
+ <option value="claude">Claude Code</option>
39
+ </select>
40
+ <select
41
+ class="session-path-select"
42
+ v-model="sessionPathFilter"
43
+ @change="onSessionPathFilterChange"
44
+ @focus="loadSessionPathOptions({ source: sessionFilterSource })"
45
+ :disabled="sessionsLoading">
46
+ <option value="">全部路径</option>
47
+ <option v-for="cwd in sessionPathOptions" :key="cwd" :value="cwd">{{ cwd }}</option>
48
+ </select>
49
+ </div>
50
+ <div class="session-toolbar-group session-toolbar-grow">
51
+ <input
52
+ class="session-query-input"
53
+ v-model="sessionQuery"
54
+ @keyup.enter="loadSessions"
55
+ :disabled="sessionsLoading || !isSessionQueryEnabled"
56
+ :placeholder="sessionQueryPlaceholder">
57
+ </div>
58
+ <div class="session-toolbar-group">
59
+ <select
60
+ class="session-role-select"
61
+ v-model="sessionRoleFilter"
62
+ @change="onSessionFilterChange"
63
+ disabled>
64
+ <option value="all">全部角色</option>
65
+ <option value="user">仅 User</option>
66
+ <option value="assistant">仅 Assistant</option>
67
+ <option value="system">仅 System</option>
68
+ </select>
69
+ <select
70
+ class="session-time-select"
71
+ v-model="sessionTimePreset"
72
+ @change="onSessionFilterChange"
73
+ disabled>
74
+ <option value="all">全部时间</option>
75
+ <option value="7d">近 7 天</option>
76
+ <option value="30d">近 30 天</option>
77
+ <option value="90d">近 90 天</option>
78
+ </select>
79
+ </div>
80
+ </div>
81
+ <div class="session-toolbar-footer">
82
+ <label class="quick-option">
83
+ <input
84
+ type="checkbox"
85
+ v-model="sessionResumeWithYolo"
86
+ @change="onSessionResumeYoloChange"
87
+ >
88
+ 复制恢复命令附带 --yolo
89
+ </label>
90
+ </div>
91
+ </div>
92
+
93
+ <div v-if="sessionsLoading" class="state-message">
94
+ 会话加载中...
95
+ </div>
96
+
97
+ <div v-else-if="sessionsList.length === 0" class="session-empty">
98
+ 暂无可用会话记录
99
+ </div>
100
+
101
+ <div v-else class="session-layout">
102
+ <div
103
+ v-if="sessionListRenderEnabled"
104
+ class="session-list"
105
+ :ref="setSessionListRef"
106
+ @scroll.passive="onSessionListScroll">
107
+ <div
108
+ v-for="session in visibleSessionsList"
109
+ :key="session.source + '-' + session.sessionId + '-' + session.filePath"
110
+ v-memo="[activeSessionExportKey === getSessionExportKey(session), session.messageCount, session.updatedAt, session.title, session.sourceLabel, isSessionPinned(session), sessionsLoading]"
111
+ :class="[
112
+ 'session-item',
113
+ {
114
+ active: activeSessionExportKey === getSessionExportKey(session),
115
+ pinned: isSessionPinned(session)
116
+ }
117
+ ]"
118
+ @click="selectSession(session)"
119
+ @keydown.enter.self.prevent="selectSession(session)"
120
+ @keydown.space.self.prevent="selectSession(session)"
121
+ tabindex="0"
122
+ role="button"
123
+ :aria-current="activeSessionExportKey === getSessionExportKey(session) ? 'true' : null">
124
+ <div class="session-item-header">
125
+ <div class="session-item-main">
126
+ <div class="session-item-title">{{ session.title || session.sessionId }}</div>
127
+ <span class="session-count-badge">{{ session.messageCount ?? 0 }}</span>
128
+ </div>
129
+ <div class="session-item-actions">
130
+ <button
131
+ class="session-item-copy session-item-pin"
132
+ @click.stop="toggleSessionPin(session)"
133
+ :disabled="sessionsLoading"
134
+ :aria-label="isSessionPinned(session) ? '取消置顶' : '置顶'"
135
+ :title="isSessionPinned(session) ? '取消置顶' : '置顶'"
136
+ :aria-pressed="isSessionPinned(session)">
137
+ <svg v-if="isSessionPinned(session)" class="pin-icon" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="1.6">
138
+ <path d="M12 22s8-6 8-12a8 8 0 1 0-16 0c0 6 8 12 8 12z"></path>
139
+ </svg>
140
+ <svg v-else class="pin-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6">
141
+ <path d="M12 22s8-6 8-12a8 8 0 1 0-16 0c0 6 8 12 8 12z"></path>
142
+ </svg>
143
+ </button>
144
+ <button
145
+ v-if="isResumeCommandAvailable(session)"
146
+ class="session-item-copy"
147
+ @click.stop="copyResumeCommand(session)"
148
+ :disabled="sessionsLoading"
149
+ aria-label="复制恢复命令"
150
+ title="复制恢复命令">
151
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
152
+ <rect x="8" y="8" width="12" height="12" rx="2"></rect>
153
+ <path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"></path>
154
+ </svg>
155
+ </button>
156
+ </div>
157
+ </div>
158
+ <div class="session-item-meta">
159
+ <span class="session-source">{{ session.sourceLabel }}</span>
160
+ <span class="session-item-time">{{ session.updatedAt || 'unknown time' }}</span>
161
+ </div>
162
+ </div>
163
+ </div>
164
+ <div v-else class="session-list session-list-placeholder"></div>
165
+
166
+ <div :class="['session-preview', { active: !!activeSession }]" :ref="setSessionPreviewContainerRef">
167
+ <template v-if="activeSession">
168
+ <div class="session-preview-scroll" :ref="setSessionPreviewScrollRef" @scroll="onSessionPreviewScroll">
169
+ <div class="session-preview-header" :ref="setSessionPreviewHeaderRef">
170
+ <div>
171
+ <div class="session-preview-title">{{ activeSession.title || activeSession.sessionId }}</div>
172
+ <div class="session-preview-meta">
173
+ <span class="session-preview-meta-item">{{ activeSession.sourceLabel }}</span>
174
+ <span class="session-preview-meta-item">{{ activeSession.updatedAt || 'unknown time' }}</span>
175
+ </div>
176
+ <div class="session-preview-meta" v-if="activeSession.cwd">
177
+ <span class="session-preview-meta-item">{{ activeSession.cwd }}</span>
178
+ </div>
179
+ </div>
180
+ <div class="session-actions">
181
+ <button class="btn-session-refresh" @click="loadActiveSessionDetail" :disabled="sessionDetailLoading || !activeSession">
182
+ {{ sessionDetailLoading ? '加载中...' : '刷新内容' }}
183
+ </button>
184
+ <button
185
+ v-if="isDeleteAvailable(activeSession)"
186
+ class="btn-session-delete"
187
+ @click="deleteSession(activeSession)"
188
+ :disabled="!activeSession || sessionsLoading || sessionDeleting[getSessionExportKey(activeSession)]">
189
+ {{ (activeSession && sessionDeleting[getSessionExportKey(activeSession)]) ? (sessionTrashEnabled === false ? '删除中...' : '移入中...') : (sessionTrashEnabled === false ? '直接删除' : '移入回收站') }}
190
+ </button>
191
+ <button
192
+ class="btn-session-export"
193
+ @click="exportSession(activeSession)"
194
+ :disabled="!activeSession || sessionExporting[getSessionExportKey(activeSession)]">
195
+ {{ (activeSession && sessionExporting[getSessionExportKey(activeSession)]) ? '导出中...' : '导出记录' }}
196
+ </button>
197
+ <button
198
+ class="btn-session-open"
199
+ @click="openSessionStandalone(activeSession)"
200
+ :disabled="!activeSession">
201
+ 新页查看
202
+ </button>
203
+ </div>
204
+ </div>
205
+
206
+ <div v-if="sessionDetailLoading && !sessionPreviewLoadingMore" class="session-preview-empty">
207
+ 正在加载会话内容...
208
+ </div>
209
+
210
+ <div v-else-if="activeSessionDetailError" class="session-preview-empty">
211
+ {{ activeSessionDetailError }}
212
+ </div>
213
+
214
+ <div v-else-if="!activeSessionMessages.length" class="session-preview-empty">
215
+ 当前会话暂无可展示消息
216
+ </div>
217
+
218
+ <div v-else-if="sessionPreviewRenderEnabled && !activeSessionVisibleMessages.length" class="session-preview-empty">
219
+ <span>正在渲染会话内容...</span>
220
+ <button class="btn-session-refresh" @click="primeSessionPreviewMessageRender" :disabled="sessionDetailLoading">
221
+ 重新渲染
222
+ </button>
223
+ </div>
224
+
225
+ <div v-else-if="!sessionPreviewRenderEnabled" class="session-preview-empty">
226
+ 正在准备会话内容...
227
+ </div>
228
+
229
+ <div v-else class="session-preview-body">
230
+ <div class="session-preview-messages">
231
+ <div v-if="activeSessionDetailClipped" class="session-item-sub session-item-wrap">
232
+ 仅展示最近 {{ activeSessionMessages.length }} 条消息。
233
+ </div>
234
+ <div
235
+ v-if="canLoadMoreSessionMessages"
236
+ class="session-item-sub session-item-wrap"
237
+ style="display:flex;align-items:center;justify-content:space-between;gap:8px;">
238
+ <span>已显示 {{ activeSessionVisibleMessages.length }} / {{ activeSessionMessages.length }} 条</span>
239
+ <button class="btn-session-refresh" @click="loadMoreSessionMessages()" :disabled="sessionDetailLoading || sessionPreviewLoadingMore">
240
+ {{ sessionPreviewLoadingMore ? '加载中...' : ('加载更多(剩余 ' + sessionPreviewRemainingCount + ')') }}
241
+ </button>
242
+ </div>
243
+ <div
244
+ v-if="sessionPreviewLoadingMore"
245
+ class="session-item-sub session-item-wrap">
246
+ 正在加载更早消息...
247
+ </div>
248
+ <div
249
+ v-for="(msg, idx) in activeSessionVisibleMessages"
250
+ :key="getRecordRenderKey(msg, idx)"
251
+ v-memo="[msg.text, msg.timestamp, msg.roleLabel, msg.normalizedRole]"
252
+ :data-message-key="getRecordRenderKey(msg, idx)"
253
+ :ref="getSessionMessageRefBinder(getRecordRenderKey(msg, idx))"
254
+ :class="['session-msg', msg.normalizedRole === 'user' ? 'user' : (msg.normalizedRole === 'system' ? 'system' : 'assistant')]">
255
+ <div class="session-msg-header">
256
+ <div class="session-msg-meta">
257
+ <span class="session-msg-role">{{ msg.roleLabel || (msg.normalizedRole === 'user' ? 'User' : (msg.normalizedRole === 'system' ? 'System' : 'Assistant')) }}</span>
258
+ <span class="session-msg-time">{{ msg.timestamp || '' }}</span>
259
+ </div>
260
+ </div>
261
+ <div class="session-msg-content">{{ msg.text || '' }}</div>
262
+ </div>
263
+ </div>
264
+ </div>
265
+ </div>
266
+ <aside v-if="sessionPreviewRenderEnabled && sessionTimelineNodes.length" class="session-timeline" aria-label="会话时间轴">
267
+ <div class="session-timeline-track"></div>
268
+ <button
269
+ v-for="node in sessionTimelineNodes"
270
+ :key="'timeline-' + node.key"
271
+ v-memo="[sessionTimelineActiveKey === node.key, node.safePercent, node.title]"
272
+ type="button"
273
+ :class="['session-timeline-node', { active: sessionTimelineActiveKey === node.key }]"
274
+ :aria-current="sessionTimelineActiveKey === node.key ? 'true' : null"
275
+ :style="{ top: `${node.safePercent}%` }"
276
+ :title="node.title"
277
+ @click="jumpToSessionTimelineNode(node.key)">
278
+ <span class="sr-only">{{ node.title }}</span>
279
+ </button>
280
+ <div class="session-timeline-current" v-if="sessionTimelineActiveTitle">
281
+ {{ sessionTimelineActiveTitle }}
282
+ </div>
283
+ </aside>
284
+ </template>
285
+
286
+ <div v-else class="session-preview-empty">
287
+ <span>请先在左侧选择一个会话</span>
288
+ </div>
289
+ </div>
290
+ </div>
291
+ </div>
292
+ </div>
@@ -0,0 +1,190 @@
1
+ <!-- 设置面板 -->
2
+ <div
3
+ v-show="mainTab === 'settings'"
4
+ class="mode-content"
5
+ id="panel-settings"
6
+ role="tabpanel"
7
+ :aria-labelledby="'tab-settings'">
8
+ <div class="config-subtabs settings-subtabs" role="tablist" aria-label="设置子标签">
9
+ <button
10
+ id="settings-tab-backup"
11
+ role="tab"
12
+ aria-controls="settings-panel-backup"
13
+ :aria-selected="settingsTab === 'backup'"
14
+ :tabindex="settingsTab === 'backup' ? 0 : -1"
15
+ :class="['config-subtab', { active: settingsTab === 'backup' }]"
16
+ @click="onSettingsTabClick('backup')">
17
+ 备份与导入
18
+ </button>
19
+ <button
20
+ id="settings-tab-trash"
21
+ role="tab"
22
+ aria-controls="settings-panel-trash"
23
+ :aria-selected="settingsTab === 'trash'"
24
+ :tabindex="settingsTab === 'trash' ? 0 : -1"
25
+ :class="['config-subtab', { active: settingsTab === 'trash' }]"
26
+ @click="onSettingsTabClick('trash')">
27
+ 回收站
28
+ <span class="settings-tab-badge">{{ sessionTrashCount }}</span>
29
+ </button>
30
+ <button
31
+ id="settings-tab-device"
32
+ role="tab"
33
+ aria-controls="settings-panel-device"
34
+ :aria-selected="settingsTab === 'device'"
35
+ :tabindex="settingsTab === 'device' ? 0 : -1"
36
+ :class="['config-subtab', { active: settingsTab === 'device' }]"
37
+ @click="onSettingsTabClick('device')">
38
+ 设备
39
+ </button>
40
+ </div>
41
+
42
+ <div
43
+ v-show="settingsTab === 'backup'"
44
+ id="settings-panel-backup"
45
+ role="tabpanel"
46
+ aria-labelledby="settings-tab-backup">
47
+ <div class="selector-section">
48
+ <div class="selector-header">
49
+ <span class="selector-title">分享命令前缀</span>
50
+ </div>
51
+ <select class="model-select" :value="shareCommandPrefix" @change="setShareCommandPrefix($event.target.value)">
52
+ <option value="npm start">npm start</option>
53
+ <option value="codexmate">codexmate</option>
54
+ </select>
55
+ <div class="config-template-hint">
56
+ 默认走项目内 <code>npm start</code>,也可切到全局 <code>codexmate</code>。该设置会缓存到浏览器本地。
57
+ </div>
58
+ </div>
59
+ <div class="selector-section">
60
+ <div class="selector-header">
61
+ <span class="selector-title">Claude 配置</span>
62
+ </div>
63
+ <button class="btn-tool" @click="downloadClaudeDirectory" :disabled="claudeDownloadLoading">
64
+ {{ claudeDownloadLoading ? ('备份中 ' + claudeDownloadProgress + '%') : '一键备份 ~/.claude' }}
65
+ </button>
66
+ <button class="btn-tool" @click="triggerClaudeImport" :disabled="claudeImportLoading">
67
+ {{ claudeImportLoading ? '导入中...' : '导入 ~/.claude 备份' }}
68
+ </button>
69
+ <input
70
+ ref="claudeImportInput"
71
+ class="sr-only"
72
+ type="file"
73
+ accept=".zip"
74
+ @change="handleClaudeImportChange">
75
+ </div>
76
+ <div class="selector-section">
77
+ <div class="selector-header">
78
+ <span class="selector-title">Codex 配置</span>
79
+ </div>
80
+ <button class="btn-tool" @click="downloadCodexDirectory" :disabled="codexDownloadLoading">
81
+ {{ codexDownloadLoading ? ('备份中 ' + codexDownloadProgress + '%') : '一键备份 ~/.codex' }}
82
+ </button>
83
+ <button class="btn-tool" @click="triggerCodexImport" :disabled="codexImportLoading">
84
+ {{ codexImportLoading ? '导入中...' : '导入 ~/.codex 备份' }}
85
+ </button>
86
+ <input
87
+ ref="codexImportInput"
88
+ class="sr-only"
89
+ type="file"
90
+ accept=".zip"
91
+ @change="handleCodexImportChange">
92
+ </div>
93
+ </div>
94
+
95
+ <div
96
+ v-show="settingsTab === 'trash'"
97
+ id="settings-panel-trash"
98
+ role="tabpanel"
99
+ aria-labelledby="settings-tab-trash">
100
+ <div class="selector-section">
101
+ <div class="selector-header">
102
+ <span class="selector-title">会话删除行为</span>
103
+ </div>
104
+ <label class="health-remote-toggle">
105
+ <input type="checkbox" :checked="sessionTrashEnabled" @change="setSessionTrashEnabled($event.target.checked)">
106
+ <span>删除会话时先移入回收站</span>
107
+ </label>
108
+ <div class="config-template-hint">
109
+ 默认开启。关闭后,会话浏览里的删除会直接永久删除,不再进入回收站。
110
+ </div>
111
+ </div>
112
+ <div class="selector-section">
113
+ <div class="selector-header settings-tab-header">
114
+ <div class="settings-tab-actions trash-header-actions">
115
+ <button class="btn-tool btn-tool-compact" @click="loadSessionTrash({ forceRefresh: true })" :disabled="sessionTrashLoading || sessionTrashClearing">
116
+ {{ sessionTrashLoading ? '刷新中...' : '刷新列表' }}
117
+ </button>
118
+ <button class="btn-tool btn-tool-compact" @click="clearSessionTrash" :disabled="sessionTrashClearing || sessionTrashLoading || !(Number(sessionTrashCount) > 0)">
119
+ {{ sessionTrashClearing ? '清空中...' : '清空回收站' }}
120
+ </button>
121
+ </div>
122
+ </div>
123
+
124
+ <div v-if="getSessionTrashViewState() === 'loading'" class="session-empty">
125
+ 正在加载回收站...
126
+ </div>
127
+ <div v-else-if="getSessionTrashViewState() === 'empty'" class="session-empty">
128
+ 回收站为空
129
+ </div>
130
+ <div v-else-if="getSessionTrashViewState() === 'retry'" class="session-empty">
131
+ 回收站列表加载失败,请刷新重试
132
+ </div>
133
+ <div v-else class="trash-list">
134
+ <div v-for="item in visibleSessionTrashItems" :key="item.trashId" class="trash-item session-item session-card">
135
+ <div class="trash-item-header session-item-header">
136
+ <div class="trash-item-main">
137
+ <div class="trash-item-mainline">
138
+ <div class="trash-item-title">{{ item.title || item.sessionId }}</div>
139
+ <span class="session-count-badge">{{ item.messageCount ?? 0 }}</span>
140
+ </div>
141
+ <div class="trash-item-meta session-item-meta">
142
+ <span class="session-source">{{ item.sourceLabel }}</span>
143
+ </div>
144
+ </div>
145
+ <div class="trash-item-side">
146
+ <div class="trash-item-actions session-item-actions">
147
+ <button class="btn-mini" @click="restoreSessionTrash(item)" :disabled="sessionTrashLoading || sessionTrashClearing || isSessionTrashActionBusy(item)">
148
+ {{ sessionTrashRestoring[getSessionTrashActionKey(item)] ? '恢复中...' : '恢复' }}
149
+ </button>
150
+ <button class="btn-mini delete" @click="purgeSessionTrash(item)" :disabled="sessionTrashLoading || sessionTrashClearing || isSessionTrashActionBusy(item)">
151
+ {{ sessionTrashPurging[getSessionTrashActionKey(item)] ? '删除中...' : '彻底删除' }}
152
+ </button>
153
+ </div>
154
+ <div class="trash-item-time session-item-time">{{ item.deletedAt || item.updatedAt || 'unknown time' }}</div>
155
+ </div>
156
+ </div>
157
+ <div v-if="item.cwd" class="trash-item-path session-item-sub session-item-wrap">
158
+ <span class="trash-item-label">工作区</span>
159
+ <span>{{ item.cwd }}</span>
160
+ </div>
161
+ <div class="trash-item-path session-item-sub session-item-wrap">
162
+ <span class="trash-item-label">原文件</span>
163
+ <span>{{ item.originalFilePath }}</span>
164
+ </div>
165
+ </div>
166
+ <div v-if="sessionTrashHasMoreItems" class="trash-list-footer">
167
+ <button class="btn-tool btn-tool-compact" @click="loadMoreSessionTrashItems" :disabled="sessionTrashLoading || sessionTrashClearing">
168
+ 加载更多(剩余 {{ sessionTrashHiddenCount }} 项)
169
+ </button>
170
+ </div>
171
+ </div>
172
+ </div>
173
+ </div>
174
+
175
+ <div
176
+ v-show="settingsTab === 'device'"
177
+ id="settings-panel-device"
178
+ role="tabpanel"
179
+ aria-labelledby="settings-tab-device">
180
+ <div class="selector-section">
181
+ <div class="selector-header">
182
+ <span class="selector-title">配置重置</span>
183
+ </div>
184
+ <div class="config-template-hint">先备份 config.toml,再写默认配置。</div>
185
+ <button class="btn-tool" @click="resetConfig" :disabled="resetConfigLoading || loading || !!initError">
186
+ {{ resetConfigLoading ? '重装中...' : '重装配置' }}
187
+ </button>
188
+ </div>
189
+ </div>
190
+ </div>