codexmate 0.0.20 → 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 -259
  2. package/README.md +284 -252
  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 -338
  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 +13101 -13497
  14. package/lib/cli-file-utils.js +151 -151
  15. package/lib/cli-models-utils.js +419 -311
  16. package/lib/cli-network-utils.js +164 -164
  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 -70
  27. package/res/json5.min.js +1 -1
  28. package/res/vue.global.prod.js +13 -0
  29. package/web-ui/app.js +530 -397
  30. package/web-ui/index.html +33 -30
  31. package/web-ui/logic.agents-diff.mjs +386 -386
  32. package/web-ui/logic.claude.mjs +168 -108
  33. package/web-ui/logic.mjs +5 -5
  34. package/web-ui/logic.runtime.mjs +124 -124
  35. package/web-ui/logic.sessions.mjs +581 -263
  36. package/web-ui/modules/api.mjs +90 -69
  37. package/web-ui/modules/app.computed.dashboard.mjs +113 -113
  38. package/web-ui/modules/app.computed.index.mjs +15 -13
  39. package/web-ui/modules/app.computed.main-tabs.mjs +195 -0
  40. package/web-ui/modules/app.computed.session.mjs +507 -141
  41. package/web-ui/modules/app.constants.mjs +15 -15
  42. package/web-ui/modules/app.methods.agents.mjs +493 -493
  43. package/web-ui/modules/app.methods.claude-config.mjs +174 -174
  44. package/web-ui/modules/app.methods.codex-config.mjs +640 -640
  45. package/web-ui/modules/app.methods.index.mjs +88 -86
  46. package/web-ui/modules/app.methods.install.mjs +149 -157
  47. package/web-ui/modules/app.methods.navigation.mjs +619 -478
  48. package/web-ui/modules/app.methods.openclaw-core.mjs +814 -514
  49. package/web-ui/modules/app.methods.openclaw-editing.mjs +372 -337
  50. package/web-ui/modules/app.methods.openclaw-persist.mjs +369 -251
  51. package/web-ui/modules/app.methods.providers.mjs +363 -265
  52. package/web-ui/modules/app.methods.runtime.mjs +323 -323
  53. package/web-ui/modules/app.methods.session-actions.mjs +520 -457
  54. package/web-ui/modules/app.methods.session-browser.mjs +626 -435
  55. package/web-ui/modules/app.methods.session-timeline.mjs +448 -441
  56. package/web-ui/modules/app.methods.session-trash.mjs +422 -419
  57. package/web-ui/modules/app.methods.startup-claude.mjs +412 -406
  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 -69
  63. package/web-ui/partials/index/layout-header.html +402 -337
  64. package/web-ui/partials/index/modal-config-template-agents.html +125 -125
  65. package/web-ui/partials/index/modal-confirm-toast.html +32 -32
  66. package/web-ui/partials/index/modal-health-check.html +72 -72
  67. package/web-ui/partials/index/modal-openclaw-config.html +280 -275
  68. package/web-ui/partials/index/modal-skills.html +184 -184
  69. package/web-ui/partials/index/modals-basic.html +156 -196
  70. package/web-ui/partials/index/panel-config-claude.html +126 -100
  71. package/web-ui/partials/index/panel-config-codex.html +237 -237
  72. package/web-ui/partials/index/panel-config-openclaw.html +78 -84
  73. package/web-ui/partials/index/panel-docs.html +130 -0
  74. package/web-ui/partials/index/panel-market.html +174 -174
  75. package/web-ui/partials/index/panel-orchestration.html +397 -0
  76. package/web-ui/partials/index/panel-sessions.html +292 -387
  77. package/web-ui/partials/index/panel-settings.html +190 -166
  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 -233
  81. package/web-ui/styles/base-theme.css +271 -373
  82. package/web-ui/styles/controls-forms.css +360 -354
  83. package/web-ui/styles/docs-panel.css +182 -0
  84. package/web-ui/styles/feedback.css +108 -108
  85. package/web-ui/styles/health-check-dialog.css +144 -144
  86. package/web-ui/styles/layout-shell.css +376 -330
  87. package/web-ui/styles/modals-core.css +464 -449
  88. package/web-ui/styles/navigation-panels.css +348 -381
  89. package/web-ui/styles/openclaw-structured.css +266 -266
  90. package/web-ui/styles/responsive.css +450 -416
  91. package/web-ui/styles/sessions-list.css +400 -414
  92. package/web-ui/styles/sessions-preview.css +411 -405
  93. package/web-ui/styles/sessions-toolbar-trash.css +243 -243
  94. package/web-ui/styles/sessions-usage.css +628 -276
  95. package/web-ui/styles/skills-list.css +296 -298
  96. package/web-ui/styles/skills-market.css +335 -335
  97. package/web-ui/styles/task-orchestration.css +776 -0
  98. package/web-ui/styles/titles-cards.css +408 -407
  99. package/web-ui/styles.css +18 -16
  100. package/web-ui.html +17 -17
  101. package/res/screenshot.png +0 -0
  102. package/res/vue.global.js +0 -18552
@@ -1,387 +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
-
24
- <div v-else>
25
- <div class="sessions-subtabs" role="tablist" aria-label="会话视图切换">
26
- <button
27
- class="sessions-subtab"
28
- :class="{ active: sessionsViewMode === 'browser' }"
29
- type="button"
30
- role="tab"
31
- :aria-selected="sessionsViewMode === 'browser'"
32
- @click="sessionsViewMode = 'browser'">
33
- Sessions
34
- </button>
35
- <button
36
- class="sessions-subtab"
37
- :class="{ active: sessionsViewMode === 'usage' }"
38
- type="button"
39
- role="tab"
40
- :aria-selected="sessionsViewMode === 'usage'"
41
- @click="sessionsViewMode = 'usage'">
42
- Usage
43
- </button>
44
- </div>
45
-
46
- <div v-if="sessionsViewMode === 'usage'">
47
- <div class="usage-toolbar">
48
- <div class="selector-header" style="padding:0;border:0;background:none;">
49
- <span class="selector-title">本地使用概览</span>
50
- </div>
51
- <div class="usage-range-group" role="group" aria-label="Usage 时间范围">
52
- <button type="button" class="usage-range-btn" :class="{ active: sessionsUsageTimeRange === '7d' }" @click="sessionsUsageTimeRange = '7d'">近 7 天</button>
53
- <button type="button" class="usage-range-btn" :class="{ active: sessionsUsageTimeRange === '30d' }" @click="sessionsUsageTimeRange = '30d'">近 30 天</button>
54
- </div>
55
- </div>
56
-
57
- <div v-if="!sessionsList.length" class="usage-empty">暂无可用于统计的会话数据</div>
58
- <template v-else>
59
- <div class="usage-summary-grid">
60
- <div v-for="card in sessionUsageSummaryCards" :key="card.key" class="usage-summary-card">
61
- <div class="usage-summary-label">{{ card.label }}</div>
62
- <div class="usage-summary-value">{{ card.value }}</div>
63
- </div>
64
- </div>
65
-
66
- <div class="usage-chart-grid">
67
- <section class="usage-card">
68
- <div class="usage-card-title">会话趋势</div>
69
- <div class="usage-legend">
70
- <span><span class="usage-legend-dot" style="background:#4f8cff"></span>Codex</span>
71
- <span><span class="usage-legend-dot" style="background:#b277ff"></span>Claude</span>
72
- </div>
73
- <div class="usage-bars">
74
- <div v-for="bucket in sessionUsageCharts.buckets" :key="bucket.key" class="usage-bar-group">
75
- <div class="usage-bar-stack">
76
- <div class="usage-bar codex" :style="{ height: ((bucket.codex / Math.max(sessionUsageCharts.maxSessionBucket, 1)) * 100) + '%' }" :title="`Codex ${bucket.codex}`"></div>
77
- <div class="usage-bar claude" :style="{ height: ((bucket.claude / Math.max(sessionUsageCharts.maxSessionBucket, 1)) * 100) + '%' }" :title="`Claude ${bucket.claude}`"></div>
78
- </div>
79
- <div class="usage-bar-label">{{ bucket.label }}</div>
80
- </div>
81
- </div>
82
- </section>
83
-
84
- <section class="usage-card">
85
- <div class="usage-card-title">来源占比</div>
86
- <div class="usage-list">
87
- <div v-for="item in sessionUsageCharts.sourceShare" :key="item.key" class="usage-list-row">
88
- <div class="usage-list-label">{{ item.label }}</div>
89
- <div class="usage-progress"><div class="usage-progress-fill" :style="{ width: item.percent + '%' }"></div></div>
90
- <div class="usage-list-value">{{ item.percent }}%</div>
91
- </div>
92
- </div>
93
- </section>
94
-
95
- <section class="usage-card">
96
- <div class="usage-card-title">消息趋势</div>
97
- <div class="usage-bars">
98
- <div v-for="bucket in sessionUsageCharts.buckets" :key="bucket.key + '-messages'" class="usage-bar-group">
99
- <div class="usage-bar-stack">
100
- <div class="usage-bar codex" style="width:100%" :style="{ height: ((bucket.totalMessages / Math.max(sessionUsageCharts.maxMessageBucket, 1)) * 100) + '%' }" :title="`${bucket.totalMessages} messages`"></div>
101
- </div>
102
- <div class="usage-bar-label">{{ bucket.label }}</div>
103
- </div>
104
- </div>
105
- </section>
106
-
107
- <section class="usage-card">
108
- <div class="usage-card-title">高频路径</div>
109
- <div v-if="!sessionUsageCharts.topPaths.length" class="usage-list-value">暂无路径数据</div>
110
- <div v-else class="usage-list">
111
- <div v-for="item in sessionUsageCharts.topPaths" :key="item.path" class="usage-list-row">
112
- <div class="usage-list-label">{{ item.count }} 次</div>
113
- <div class="usage-progress"><div class="usage-progress-fill" :style="{ width: ((item.count / Math.max(sessionUsageCharts.topPaths[0]?.count || 1, 1)) * 100) + '%' }"></div></div>
114
- <div class="usage-list-value" :title="item.path">{{ item.path }}</div>
115
- </div>
116
- </div>
117
- </section>
118
- </div>
119
- </template>
120
- </div>
121
-
122
- <template v-else>
123
- <div class="selector-section">
124
- <div class="selector-header">
125
- <span class="selector-title">会话来源</span>
126
- <div class="selector-actions">
127
- <button class="btn-tool btn-tool-compact" @click="loadSessions" :disabled="sessionsLoading">
128
- {{ sessionsLoading ? '刷新中...' : '刷新会话' }}
129
- </button>
130
- </div>
131
- </div>
132
- <div class="session-toolbar">
133
- <div class="session-toolbar-group">
134
- <select class="session-source-select" v-model="sessionFilterSource" @change="onSessionSourceChange" :disabled="sessionsLoading">
135
- <option value="all">全部</option>
136
- <option value="codex">Codex</option>
137
- <option value="claude">Claude Code</option>
138
- </select>
139
- <select
140
- class="session-path-select"
141
- v-model="sessionPathFilter"
142
- @change="onSessionPathFilterChange"
143
- :disabled="sessionsLoading">
144
- <option value="">全部路径</option>
145
- <option v-for="cwd in sessionPathOptions" :key="cwd" :value="cwd">{{ cwd }}</option>
146
- </select>
147
- </div>
148
- <div class="session-toolbar-group session-toolbar-grow">
149
- <input
150
- class="session-query-input"
151
- v-model="sessionQuery"
152
- @keyup.enter="loadSessions"
153
- :disabled="sessionsLoading || !isSessionQueryEnabled"
154
- :placeholder="sessionQueryPlaceholder">
155
- </div>
156
- <div class="session-toolbar-group">
157
- <select
158
- class="session-role-select"
159
- v-model="sessionRoleFilter"
160
- @change="onSessionFilterChange"
161
- disabled>
162
- <option value="all">全部角色</option>
163
- <option value="user">仅 User</option>
164
- <option value="assistant">仅 Assistant</option>
165
- <option value="system">仅 System</option>
166
- </select>
167
- <select
168
- class="session-time-select"
169
- v-model="sessionTimePreset"
170
- @change="onSessionFilterChange"
171
- disabled>
172
- <option value="all">全部时间</option>
173
- <option value="7d">近 7 天</option>
174
- <option value="30d">近 30 天</option>
175
- <option value="90d">近 90 天</option>
176
- </select>
177
- </div>
178
- </div>
179
- <div class="session-toolbar-footer">
180
- <label class="quick-option">
181
- <input
182
- type="checkbox"
183
- v-model="sessionResumeWithYolo"
184
- @change="onSessionResumeYoloChange"
185
- >
186
- 复制恢复命令附带 --yolo
187
- </label>
188
- </div>
189
- </div>
190
-
191
- <div v-if="sessionsLoading" class="state-message">
192
- 会话加载中...
193
- </div>
194
-
195
- <div v-else-if="sessionsList.length === 0" class="session-empty">
196
- 暂无可用会话记录
197
- </div>
198
-
199
- <div v-else class="session-layout">
200
- <div v-if="sessionListRenderEnabled" class="session-list">
201
- <div
202
- v-for="session in sortedSessionsList"
203
- :key="session.source + '-' + session.sessionId + '-' + session.filePath"
204
- v-memo="[activeSessionExportKey === getSessionExportKey(session), session.messageCount, session.updatedAt, session.title, session.sourceLabel, isSessionPinned(session), sessionsLoading]"
205
- :class="[
206
- 'session-item',
207
- {
208
- active: activeSessionExportKey === getSessionExportKey(session),
209
- pinned: isSessionPinned(session)
210
- }
211
- ]"
212
- @click="selectSession(session)"
213
- @keydown.enter.self.prevent="selectSession(session)"
214
- @keydown.space.self.prevent="selectSession(session)"
215
- tabindex="0"
216
- role="button"
217
- :aria-current="activeSessionExportKey === getSessionExportKey(session) ? 'true' : null">
218
- <div class="session-item-header">
219
- <div class="session-item-main">
220
- <div class="session-item-title">{{ session.title || session.sessionId }}</div>
221
- <span class="session-count-badge">{{ session.messageCount ?? 0 }}</span>
222
- </div>
223
- <div class="session-item-actions">
224
- <button
225
- class="session-item-copy session-item-pin"
226
- @click.stop="toggleSessionPin(session)"
227
- :disabled="sessionsLoading"
228
- :aria-label="isSessionPinned(session) ? '取消置顶' : '置顶'"
229
- :title="isSessionPinned(session) ? '取消置顶' : '置顶'"
230
- :aria-pressed="isSessionPinned(session)">
231
- <svg v-if="isSessionPinned(session)" class="pin-icon" viewBox="0 0 24 24" fill="currentColor" stroke="currentColor" stroke-width="1.6">
232
- <path d="M12 22s8-6 8-12a8 8 0 1 0-16 0c0 6 8 12 8 12z"></path>
233
- </svg>
234
- <svg v-else class="pin-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.6">
235
- <path d="M12 22s8-6 8-12a8 8 0 1 0-16 0c0 6 8 12 8 12z"></path>
236
- </svg>
237
- </button>
238
- <button
239
- v-if="isResumeCommandAvailable(session)"
240
- class="session-item-copy"
241
- @click.stop="copyResumeCommand(session)"
242
- :disabled="sessionsLoading"
243
- aria-label="复制恢复命令"
244
- title="复制恢复命令">
245
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
246
- <rect x="8" y="8" width="12" height="12" rx="2"></rect>
247
- <path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2"></path>
248
- </svg>
249
- </button>
250
- </div>
251
- </div>
252
- <div class="session-item-meta">
253
- <span class="session-source">{{ session.sourceLabel }}</span>
254
- <span class="session-item-time">{{ session.updatedAt || 'unknown time' }}</span>
255
- </div>
256
- </div>
257
- </div>
258
- <div v-else class="session-list session-list-placeholder"></div>
259
-
260
- <div :class="['session-preview', { active: !!activeSession }]" :ref="setSessionPreviewContainerRef">
261
- <template v-if="activeSession">
262
- <div class="session-preview-scroll" :ref="setSessionPreviewScrollRef" @scroll="onSessionPreviewScroll">
263
- <div class="session-preview-header" :ref="setSessionPreviewHeaderRef">
264
- <div>
265
- <div class="session-preview-title">{{ activeSession.title || activeSession.sessionId }}</div>
266
- <div class="session-preview-meta">
267
- <span class="session-preview-meta-item">{{ activeSession.sourceLabel }}</span>
268
- <span class="session-preview-meta-item">{{ activeSession.updatedAt || 'unknown time' }}</span>
269
- </div>
270
- <div class="session-preview-meta" v-if="activeSession.cwd">
271
- <span class="session-preview-meta-item">{{ activeSession.cwd }}</span>
272
- </div>
273
- </div>
274
- <div class="session-actions">
275
- <button class="btn-session-refresh" @click="loadActiveSessionDetail" :disabled="sessionDetailLoading || !activeSession">
276
- {{ sessionDetailLoading ? '加载中...' : '刷新内容' }}
277
- </button>
278
- <button
279
- v-if="isDeleteAvailable(activeSession)"
280
- class="btn-session-delete"
281
- @click="deleteSession(activeSession)"
282
- :disabled="!activeSession || sessionsLoading || sessionDeleting[getSessionExportKey(activeSession)]">
283
- {{ (activeSession && sessionDeleting[getSessionExportKey(activeSession)]) ? '移入中...' : '移入回收站' }}
284
- </button>
285
- <button
286
- class="btn-session-export"
287
- @click="exportSession(activeSession)"
288
- :disabled="!activeSession || sessionExporting[getSessionExportKey(activeSession)]">
289
- {{ (activeSession && sessionExporting[getSessionExportKey(activeSession)]) ? '导出中...' : '导出记录' }}
290
- </button>
291
- <button
292
- class="btn-session-open"
293
- @click="openSessionStandalone(activeSession)"
294
- :disabled="!activeSession">
295
- 新页查看
296
- </button>
297
- </div>
298
- </div>
299
-
300
- <div v-if="sessionDetailLoading && !sessionPreviewLoadingMore" class="session-preview-empty">
301
- 正在加载会话内容...
302
- </div>
303
-
304
- <div v-else-if="activeSessionDetailError" class="session-preview-empty">
305
- {{ activeSessionDetailError }}
306
- </div>
307
-
308
- <div v-else-if="!activeSessionMessages.length" class="session-preview-empty">
309
- 当前会话暂无可展示消息
310
- </div>
311
-
312
- <div v-else-if="sessionPreviewRenderEnabled && !activeSessionVisibleMessages.length" class="session-preview-empty">
313
- <span>正在渲染会话内容...</span>
314
- <button class="btn-session-refresh" @click="primeSessionPreviewMessageRender" :disabled="sessionDetailLoading">
315
- 重新渲染
316
- </button>
317
- </div>
318
-
319
- <div v-else-if="!sessionPreviewRenderEnabled" class="session-preview-empty">
320
- 正在准备会话内容...
321
- </div>
322
-
323
- <div v-else class="session-preview-body">
324
- <div class="session-preview-messages">
325
- <div v-if="activeSessionDetailClipped" class="session-item-sub session-item-wrap">
326
- 仅展示最近 {{ activeSessionMessages.length }} 条消息。
327
- </div>
328
- <div
329
- v-if="canLoadMoreSessionMessages"
330
- class="session-item-sub session-item-wrap"
331
- style="display:flex;align-items:center;justify-content:space-between;gap:8px;">
332
- <span>已显示 {{ activeSessionVisibleMessages.length }} / {{ activeSessionMessages.length }} 条</span>
333
- <button class="btn-session-refresh" @click="loadMoreSessionMessages()" :disabled="sessionDetailLoading || sessionPreviewLoadingMore">
334
- {{ sessionPreviewLoadingMore ? '加载中...' : ('加载更多(剩余 ' + sessionPreviewRemainingCount + ')') }}
335
- </button>
336
- </div>
337
- <div
338
- v-if="sessionPreviewLoadingMore"
339
- class="session-item-sub session-item-wrap">
340
- 正在加载更早消息...
341
- </div>
342
- <div
343
- v-for="(msg, idx) in activeSessionVisibleMessages"
344
- :key="getRecordRenderKey(msg, idx)"
345
- v-memo="[msg.text, msg.timestamp, msg.roleLabel, msg.normalizedRole]"
346
- :data-message-key="getRecordRenderKey(msg, idx)"
347
- :ref="getSessionMessageRefBinder(getRecordRenderKey(msg, idx))"
348
- :class="['session-msg', msg.normalizedRole === 'user' ? 'user' : (msg.normalizedRole === 'system' ? 'system' : 'assistant')]">
349
- <div class="session-msg-header">
350
- <div class="session-msg-meta">
351
- <span class="session-msg-role">{{ msg.roleLabel || (msg.normalizedRole === 'user' ? 'User' : (msg.normalizedRole === 'system' ? 'System' : 'Assistant')) }}</span>
352
- <span class="session-msg-time">{{ msg.timestamp || '' }}</span>
353
- </div>
354
- </div>
355
- <div class="session-msg-content">{{ msg.text || '' }}</div>
356
- </div>
357
- </div>
358
- </div>
359
- </div>
360
- <aside v-if="sessionPreviewRenderEnabled && sessionTimelineNodes.length" class="session-timeline" aria-label="会话时间轴">
361
- <div class="session-timeline-track"></div>
362
- <button
363
- v-for="node in sessionTimelineNodes"
364
- :key="'timeline-' + node.key"
365
- v-memo="[sessionTimelineActiveKey === node.key, node.safePercent, node.title]"
366
- type="button"
367
- :class="['session-timeline-node', { active: sessionTimelineActiveKey === node.key }]"
368
- :aria-current="sessionTimelineActiveKey === node.key ? 'true' : null"
369
- :style="{ top: `${node.safePercent}%` }"
370
- :title="node.title"
371
- @click="jumpToSessionTimelineNode(node.key)">
372
- <span class="sr-only">{{ node.title }}</span>
373
- </button>
374
- <div class="session-timeline-current" v-if="sessionTimelineActiveTitle">
375
- {{ sessionTimelineActiveTitle }}
376
- </div>
377
- </aside>
378
- </template>
379
-
380
- <div v-else class="session-preview-empty">
381
- <span>请先在左侧选择一个会话</span>
382
- </div>
383
- </div>
384
- </div>
385
- </template>
386
- </div>
387
- </div>
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>