codexmate 0.1.8 → 0.1.9

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexmate",
3
- "version": "0.1.8",
3
+ "version": "0.1.9",
4
4
  "description": "Codex/Claude Code/OpenClaw 配置、会话与任务编排 CLI + Web 工具",
5
5
  "main": "cli.js",
6
6
  "bin": {
package/web-ui/app.js CHANGED
@@ -240,6 +240,9 @@ document.addEventListener('DOMContentLoaded', () => {
240
240
  sessionImportingNative: {},
241
241
  sessionCloning: {},
242
242
  sessionDeleting: {},
243
+ sessionDeletingSelected: false,
244
+ sessionBatchSelectMode: false,
245
+ sessionSelectedKeys: {},
243
246
  activeSession: null,
244
247
  activeSessionMessages: [],
245
248
  activeSessionDetailError: '',
@@ -639,13 +639,14 @@ export function createSessionActionMethods(options = {}) {
639
639
  }
640
640
  },
641
641
 
642
- async deleteSession(session) {
642
+ async deleteSession(session, batchOpts = {}) {
643
+ const batch = !!(batchOpts && batchOpts.batch);
643
644
  if (!this.isDeleteAvailable(session)) {
644
- this.showMessage('不支持此操作', 'error');
645
- return;
645
+ if (!batch) this.showMessage('不支持此操作', 'error');
646
+ return false;
646
647
  }
647
648
  const useTrash = this.sessionTrashEnabled !== false;
648
- if (!useTrash && typeof this.requestConfirmDialog === 'function') {
649
+ if (!batch && !useTrash && typeof this.requestConfirmDialog === 'function') {
649
650
  const confirmed = await this.requestConfirmDialog({
650
651
  title: '直接删除会话',
651
652
  message: '关闭回收站后,删除会话将直接永久删除,且无法恢复。',
@@ -654,12 +655,12 @@ export function createSessionActionMethods(options = {}) {
654
655
  danger: true
655
656
  });
656
657
  if (!confirmed) {
657
- return;
658
+ return false;
658
659
  }
659
660
  }
660
661
  const key = this.getSessionExportKey(session);
661
662
  if (this.sessionDeleting[key]) {
662
- return;
663
+ return false;
663
664
  }
664
665
  this.sessionDeleting[key] = true;
665
666
  try {
@@ -670,13 +671,14 @@ export function createSessionActionMethods(options = {}) {
670
671
  filePath: session.filePath
671
672
  });
672
673
  if (!res || res.error) {
673
- this.showMessage((res && res.error) || '删除失败', 'error');
674
- return;
674
+ const msg = (res && res.error) || (typeof this.t === 'function' ? this.t('toast.delete.fail') : '删除失败');
675
+ if (!batch) this.showMessage(msg, 'error');
676
+ return false;
675
677
  }
676
678
  this.removeSessionPin(session);
677
679
  if (useTrash) {
678
680
  this.invalidateSessionTrashRequests();
679
- this.showMessage('已移入回收站', 'success');
681
+ if (!batch) this.showMessage('已移入回收站', 'success');
680
682
  if (this.sessionTrashLoadedOnce) {
681
683
  this.prependSessionTrashItem(this.buildSessionTrashItemFromSession(session, res), {
682
684
  totalCount: res && res.totalCount !== undefined ? res.totalCount : undefined
@@ -689,7 +691,7 @@ export function createSessionActionMethods(options = {}) {
689
691
  this.sessionTrashItems
690
692
  );
691
693
  }
692
- } else {
694
+ } else if (!batch) {
693
695
  this.showMessage('已删除', 'success');
694
696
  }
695
697
  if (typeof this.invalidateSessionsUsageData === 'function') {
@@ -700,11 +702,159 @@ export function createSessionActionMethods(options = {}) {
700
702
  } catch (_) {
701
703
  // The delete already succeeded remotely; keep the success result.
702
704
  }
705
+ return true;
703
706
  } catch (_) {
704
- this.showMessage(this.t('toast.delete.fail'), 'error');
707
+ if (!batch) {
708
+ this.showMessage(this.t && this.t('toast.delete.fail') ? this.t('toast.delete.fail') : '删除失败', 'error');
709
+ }
710
+ return false;
705
711
  } finally {
706
712
  this.sessionDeleting[key] = false;
707
713
  }
714
+ },
715
+
716
+ isSessionBatchSelectable(session) {
717
+ return this.isDeleteAvailable(session);
718
+ },
719
+
720
+ isSessionSelectedForBatch(session) {
721
+ const key = this.getSessionExportKey(session);
722
+ if (!key) return false;
723
+ return !!this.sessionSelectedKeys[key];
724
+ },
725
+
726
+ toggleSessionSelectionForBatch(session) {
727
+ const key = this.getSessionExportKey(session);
728
+ if (!key) return;
729
+ if (this.sessionSelectedKeys[key]) {
730
+ const next = { ...this.sessionSelectedKeys };
731
+ delete next[key];
732
+ this.sessionSelectedKeys = next;
733
+ return;
734
+ }
735
+ this.sessionSelectedKeys = { ...this.sessionSelectedKeys, [key]: true };
736
+ },
737
+
738
+ enterSessionBatchSelectMode() {
739
+ if (this.sessionBatchSelectMode) return;
740
+ this.sessionBatchSelectMode = true;
741
+ this.sessionSelectedKeys = {};
742
+ },
743
+
744
+ exitSessionBatchSelectMode() {
745
+ if (!this.sessionBatchSelectMode) return;
746
+ this.sessionBatchSelectMode = false;
747
+ this.sessionSelectedKeys = {};
748
+ },
749
+
750
+ getSelectedBatchCount() {
751
+ return this.sessionsList.filter(s => this.sessionBatchSelectMode && this.isSessionSelectedForBatch(s)).length;
752
+ },
753
+
754
+ getVisibleBatchSelectableCount() {
755
+ return (this.visibleSessionsList || []).filter(s => this.isDeleteAvailable(s)).length;
756
+ },
757
+
758
+ isAllVisibleBatchSelected() {
759
+ const visible = (this.visibleSessionsList || []).filter(s => this.isDeleteAvailable(s));
760
+ if (visible.length === 0) return false;
761
+ return visible.every(s => this.isSessionSelectedForBatch(s) || this.sessionDeleting[this.getSessionExportKey(s)]);
762
+ },
763
+
764
+ toggleSelectAllVisibleForBatch() {
765
+ const visible = (this.visibleSessionsList || []).filter(s => this.isDeleteAvailable(s) && !this.sessionDeleting[this.getSessionExportKey(s)]);
766
+ if (visible.length === 0) return;
767
+ const allSelected = visible.every(s => this.isSessionSelectedForBatch(s));
768
+ const next = { ...this.sessionSelectedKeys };
769
+ if (allSelected) {
770
+ for (const s of visible) {
771
+ delete next[this.getSessionExportKey(s)];
772
+ }
773
+ } else {
774
+ for (const s of visible) {
775
+ next[this.getSessionExportKey(s)] = true;
776
+ }
777
+ }
778
+ this.sessionSelectedKeys = next;
779
+ },
780
+
781
+ clearSessionBatchSelection() {
782
+ this.sessionSelectedKeys = {};
783
+ },
784
+
785
+ async deleteSelectedSessions() {
786
+ if (this.sessionDeletingSelected) return;
787
+ const selected = (this.sessionsList || []).filter(s => this.isSessionSelectedForBatch(s));
788
+ if (selected.length === 0) {
789
+ if (typeof this.t === 'function') {
790
+ this.showMessage(this.t('sessions.batch.emptySelection'), 'warning');
791
+ }
792
+ return;
793
+ }
794
+ const useTrash = this.sessionTrashEnabled !== false;
795
+ if (typeof this.requestConfirmDialog === 'function') {
796
+ const confirmKey = useTrash ? 'sessions.batch.deleteConfirm' : 'sessions.batch.deleteConfirmHard';
797
+ const confirmed = await this.requestConfirmDialog({
798
+ title: this.t('sessions.batch.select'),
799
+ message: this.t(confirmKey, { count: selected.length }),
800
+ confirmText: this.t('sessions.batch.deleteSelected'),
801
+ cancelText: this.t('common.cancel'),
802
+ danger: !useTrash
803
+ });
804
+ if (!confirmed) {
805
+ return;
806
+ }
807
+ }
808
+ this.sessionDeletingSelected = true;
809
+ let failed = 0;
810
+ const succeededKeys = [];
811
+ try {
812
+ for (const session of selected) {
813
+ const ok = await this.deleteSession(session, { batch: true });
814
+ if (ok) {
815
+ succeededKeys.push(this.getSessionExportKey(session));
816
+ } else {
817
+ failed += 1;
818
+ }
819
+ }
820
+ if (succeededKeys.length > 0) {
821
+ if (typeof this.invalidateSessionsUsageData === 'function') {
822
+ this.invalidateSessionsUsageData({ preserveList: true });
823
+ }
824
+ this.invalidateSessionTrashRequests();
825
+ // Remove succeeded sessions from the current list in order
826
+ for (const key of succeededKeys) {
827
+ const session = (this.sessionsList || []).find(s => this.getSessionExportKey(s) === key);
828
+ if (session) {
829
+ try {
830
+ await this.removeSessionFromCurrentList(session);
831
+ } catch (_) {
832
+ // Remote delete already succeeded; ignore local cleanup errors.
833
+ }
834
+ }
835
+ }
836
+ // Clear succeeded selections; keep failed ones selected
837
+ const next = {};
838
+ for (const session of selected) {
839
+ const k = this.getSessionExportKey(session);
840
+ if (failed > 0 && !succeededKeys.includes(k)) {
841
+ next[k] = true;
842
+ }
843
+ }
844
+ this.sessionSelectedKeys = next;
845
+ }
846
+ if (failed === 0) {
847
+ const okMsg = useTrash ? '已移入回收站' : '已删除';
848
+ this.showMessage(okMsg + ' · ' + succeededKeys.length, 'success');
849
+ this.sessionBatchSelectMode = false;
850
+ } else if (failed === selected.length) {
851
+ this.showMessage(this.t('sessions.batch.allFail'), 'error');
852
+ } else {
853
+ this.showMessage(this.t('sessions.batch.partialFail', { failed }), 'warning');
854
+ }
855
+ } finally {
856
+ this.sessionDeletingSelected = false;
857
+ }
708
858
  }
709
859
  };
710
860
  }
@@ -303,6 +303,7 @@ export function createSessionBrowserMethods(options = {}) {
303
303
  },
304
304
 
305
305
  onSessionSortChange() {
306
+ this.exitSessionBatchSelectMode();
306
307
  const normalized = this.normalizeSessionSortMode(this.sessionSortMode);
307
308
  this.sessionSortMode = normalized;
308
309
  if (typeof this.persistWebUiPreferences === 'function') {
@@ -445,6 +446,7 @@ export function createSessionBrowserMethods(options = {}) {
445
446
 
446
447
  setSessionSource(value) {
447
448
  if (this.sessionsLoading) return;
449
+ this.exitSessionBatchSelectMode();
448
450
  this.sessionFilterSource = value;
449
451
  this.refreshSessionPathOptions(value);
450
452
  this.persistSessionFilterCache();
@@ -478,12 +480,14 @@ export function createSessionBrowserMethods(options = {}) {
478
480
  },
479
481
 
480
482
  async onSessionPathFilterChange() {
483
+ this.exitSessionBatchSelectMode();
481
484
  this.persistSessionFilterCache();
482
485
  syncSessionsFilterUrl(this);
483
486
  await this.loadSessions();
484
487
  },
485
488
 
486
489
  async onSessionFilterChange() {
490
+ this.exitSessionBatchSelectMode();
487
491
  this.persistSessionFilterCache();
488
492
  syncSessionsFilterUrl(this);
489
493
  await this.loadSessions();
@@ -552,6 +556,7 @@ export function createSessionBrowserMethods(options = {}) {
552
556
  },
553
557
 
554
558
  async clearSessionFilters() {
559
+ this.exitSessionBatchSelectMode();
555
560
  this.sessionFilterSource = 'all';
556
561
  this.sessionPathFilter = '';
557
562
  this.sessionQuery = '';
@@ -664,6 +664,19 @@ const en = Object.freeze({
664
664
  'sessions.copyResume': 'Copy resume command',
665
665
  'sessions.preview.refresh': 'Refresh content',
666
666
  'sessions.preview.loading': 'Loading...',
667
+ 'sessions.batch.select': 'Batch delete',
668
+ 'sessions.batch.exit': 'Exit selection',
669
+ 'sessions.batch.selectAll': 'Select all',
670
+ 'sessions.batch.unselectAll': 'Clear selection',
671
+ 'sessions.batch.deleteSelected': 'Delete selected',
672
+ 'sessions.batch.deleteSelectedN': 'Delete selected ({count})',
673
+ 'sessions.batch.deletingSelected': 'Deleting...',
674
+ 'sessions.batch.deleteConfirm': 'Delete the {count} selected sessions?',
675
+ 'sessions.batch.deleteConfirmHard': 'Permanently delete the {count} selected sessions? This cannot be undone.',
676
+ 'sessions.batch.deleteHint': '{count} selected',
677
+ 'sessions.batch.emptySelection': 'Select sessions to delete first',
678
+ 'sessions.batch.partialFail': '{failed} failed to delete; selection kept',
679
+ 'sessions.batch.allFail': 'Delete failed, please retry',
667
680
  'sessions.preview.deleteHard': 'Delete permanently',
668
681
  'sessions.preview.moveToTrash': 'Move to trash',
669
682
  'sessions.preview.deleting': 'Deleting...',
@@ -666,6 +666,19 @@ const ja = Object.freeze({
666
666
  'sessions.copyResume': '再開コマンドをコピー',
667
667
  'sessions.preview.refresh': '更新',
668
668
  'sessions.preview.loading': 'プレビュー読み込み中...',
669
+ 'sessions.batch.select': '一括削除',
670
+ 'sessions.batch.exit': '選択を終了',
671
+ 'sessions.batch.selectAll': 'すべて選択',
672
+ 'sessions.batch.unselectAll': '選択を解除',
673
+ 'sessions.batch.deleteSelected': '選択を削除',
674
+ 'sessions.batch.deleteSelectedN': '選択を削除({count})',
675
+ 'sessions.batch.deletingSelected': '削除中...',
676
+ 'sessions.batch.deleteConfirm': '選択した {count} 件のセッションを削除しますか?',
677
+ 'sessions.batch.deleteConfirmHard': '選択した {count} 件のセッションを完全に削除しますか?元に戻せません。',
678
+ 'sessions.batch.deleteHint': '{count} 件選択',
679
+ 'sessions.batch.emptySelection': '削除するセッションを選択してください',
680
+ 'sessions.batch.partialFail': '{failed} 件の削除に失敗しました。選択を維持します',
681
+ 'sessions.batch.allFail': '削除に失敗しました。再試行してください',
669
682
  'sessions.preview.deleteHard': '直接削除',
670
683
  'sessions.preview.moveToTrash': 'ゴミ箱に移動',
671
684
  'sessions.preview.deleting': '削除中...',
@@ -872,6 +872,19 @@ const vi = Object.freeze({
872
872
  'sessions.copyResume': 'Sao chép lệnh tiếp tục',
873
873
  'sessions.preview.refresh': 'Làm mới nội dung',
874
874
  'sessions.preview.loading': 'Đang tải...',
875
+ 'sessions.batch.select': 'Xóa hàng loạt',
876
+ 'sessions.batch.exit': 'Thoát chọn',
877
+ 'sessions.batch.selectAll': 'Chọn tất cả',
878
+ 'sessions.batch.unselectAll': 'Bỏ chọn tất cả',
879
+ 'sessions.batch.deleteSelected': 'Xóa đã chọn',
880
+ 'sessions.batch.deleteSelectedN': 'Xóa đã chọn ({count})',
881
+ 'sessions.batch.deletingSelected': 'Đang xóa...',
882
+ 'sessions.batch.deleteConfirm': 'Xóa {count} phiên đã chọn?',
883
+ 'sessions.batch.deleteConfirmHard': 'Xóa vĩnh viễn {count} phiên đã chọn? Không thể hoàn tác.',
884
+ 'sessions.batch.deleteHint': 'Đã chọn {count}',
885
+ 'sessions.batch.emptySelection': 'Hãy chọn phiên cần xóa trước',
886
+ 'sessions.batch.partialFail': '{failed} mục xóa thất bại, giữ nguyên lựa chọn',
887
+ 'sessions.batch.allFail': 'Xóa thất bại, vui lòng thử lại',
875
888
  'sessions.preview.deleteHard': 'Xóa vĩnh viễn',
876
889
  'sessions.preview.moveToTrash': 'Chuyển vào thùng rác',
877
890
  'sessions.preview.deleting': 'Đang xóa...',
@@ -663,6 +663,19 @@ const zhTw = Object.freeze({
663
663
  'sessions.copyResume': '複製恢復命令',
664
664
  'sessions.preview.refresh': '重新整理內容',
665
665
  'sessions.preview.loading': '載入中...',
666
+ 'sessions.batch.select': '批次刪除',
667
+ 'sessions.batch.exit': '退出選擇',
668
+ 'sessions.batch.selectAll': '全選',
669
+ 'sessions.batch.unselectAll': '取消全選',
670
+ 'sessions.batch.deleteSelected': '刪除選中',
671
+ 'sessions.batch.deleteSelectedN': '刪除選中({count})',
672
+ 'sessions.batch.deletingSelected': '刪除中...',
673
+ 'sessions.batch.deleteConfirm': '確定刪除選中的 {count} 個會話嗎?',
674
+ 'sessions.batch.deleteConfirmHard': '確定永久刪除選中的 {count} 個會話嗎?該操作不可還原。',
675
+ 'sessions.batch.deleteHint': '已選 {count} 個',
676
+ 'sessions.batch.emptySelection': '請先選擇要刪除的會話',
677
+ 'sessions.batch.partialFail': '其中 {failed} 個刪除失敗,已保留選中',
678
+ 'sessions.batch.allFail': '刪除失敗,請重試',
666
679
  'sessions.preview.deleteHard': '直接刪除',
667
680
  'sessions.preview.moveToTrash': '移入回收站',
668
681
  'sessions.preview.deleting': '刪除中...',
@@ -663,6 +663,19 @@ const zh = Object.freeze({
663
663
  'sessions.copyResume': '复制恢复命令',
664
664
  'sessions.preview.refresh': '刷新内容',
665
665
  'sessions.preview.loading': '加载中...',
666
+ 'sessions.batch.select': '批量删除',
667
+ 'sessions.batch.exit': '退出选择',
668
+ 'sessions.batch.selectAll': '全选',
669
+ 'sessions.batch.unselectAll': '取消全选',
670
+ 'sessions.batch.deleteSelected': '删除选中',
671
+ 'sessions.batch.deleteSelectedN': '删除选中({count})',
672
+ 'sessions.batch.deletingSelected': '删除中...',
673
+ 'sessions.batch.deleteConfirm': '确定删除选中的 {count} 个会话吗?',
674
+ 'sessions.batch.deleteConfirmHard': '确定永久删除选中的 {count} 个会话吗?该操作不可恢复。',
675
+ 'sessions.batch.deleteHint': '已选 {count} 个',
676
+ 'sessions.batch.emptySelection': '请先选择要删除的会话',
677
+ 'sessions.batch.partialFail': '其中 {failed} 个删除失败,已保留选中',
678
+ 'sessions.batch.allFail': '删除失败,请重试',
666
679
  'sessions.preview.deleteHard': '直接删除',
667
680
  'sessions.preview.moveToTrash': '移入回收站',
668
681
  'sessions.preview.deleting': '删除中...',
@@ -103,6 +103,22 @@
103
103
  <button class="btn-tool btn-tool-compact" type="button" @click="clearSessionFilters" :disabled="sessionsLoading">
104
104
  {{ t('common.resetFilters') }}
105
105
  </button>
106
+ <button v-if="!sessionBatchSelectMode" class="btn-tool btn-tool-compact btn-tool-batch" type="button" @click="enterSessionBatchSelectMode" :disabled="sessionsLoading" :title="t('sessions.batch.select')">
107
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M7 6V4a2 2 0 012-2h6a2 2 0 012 2v2M10 11v6M14 11v6"/><path d="M5 6l1 14a2 2 0 002 2h8a2 2 0 002-2l1-14"/></svg>
108
+ <span>{{ t('sessions.batch.select') }}</span>
109
+ </button>
110
+ <template v-else>
111
+ <button class="btn-tool btn-tool-compact" type="button" @click="toggleSelectAllVisibleForBatch" :disabled="sessionsLoading" :title="isAllVisibleBatchSelected() ? t('sessions.batch.unselectAll') : t('sessions.batch.selectAll')">
112
+ {{ isAllVisibleBatchSelected() ? t('sessions.batch.unselectAll') : t('sessions.batch.selectAll') }}
113
+ </button>
114
+ <button class="btn-tool btn-tool-compact btn-tool-batch-delete" type="button" @click="deleteSelectedSessions" :disabled="sessionsLoading || sessionDeletingSelected || getSelectedBatchCount() === 0" :title="getSelectedBatchCount() > 0 ? t('sessions.batch.deleteSelectedN', { count: getSelectedBatchCount() }) : t('sessions.batch.emptySelection')">
115
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"/><path d="M7 6V4a2 2 0 012-2h6a2 2 0 012 2v2M10 11v6M14 11v6"/><path d="M5 6l1 14a2 2 0 002 2h8a2 2 0 002-2l1-14"/></svg>
116
+ <span>{{ sessionDeletingSelected ? t('sessions.batch.deletingSelected') : (getSelectedBatchCount() > 0 ? t('sessions.batch.deleteSelectedN', { count: getSelectedBatchCount() }) : t('sessions.batch.deleteSelected')) }}</span>
117
+ </button>
118
+ <button class="btn-tool btn-tool-compact" type="button" @click="exitSessionBatchSelectMode" :disabled="sessionDeletingSelected" :title="t('sessions.batch.exit')">
119
+ {{ t('sessions.batch.exit') }}
120
+ </button>
121
+ </template>
106
122
  </div>
107
123
  </div>
108
124
  <div v-if="hasActiveSessionFilters()" class="session-filter-chips" :aria-label="t('sessions.filters.copyLink')">
@@ -137,12 +153,14 @@
137
153
  <div
138
154
  v-for="session in visibleSessionsList"
139
155
  :key="session.source + '-' + session.sessionId + '-' + session.filePath"
140
- v-memo="[activeSessionExportKey === getSessionExportKey(session), session.messageCount, session.updatedAt, session.title, session.sourceLabel, session.cwd, isSessionPinned(session), sessionsLoading, session.match && session.match.count]"
156
+ v-memo="[activeSessionExportKey === getSessionExportKey(session), session.messageCount, session.updatedAt, session.title, session.sourceLabel, session.cwd, isSessionPinned(session), sessionsLoading, session.match && session.match.count, sessionBatchSelectMode, isSessionSelectedForBatch(session)]"
141
157
  :class="[
142
158
  'session-item',
143
159
  {
144
160
  active: activeSessionExportKey === getSessionExportKey(session),
145
- pinned: isSessionPinned(session)
161
+ pinned: isSessionPinned(session),
162
+ 'batch-selected': sessionBatchSelectMode && isSessionSelectedForBatch(session),
163
+ 'batch-selectable': sessionBatchSelectMode
146
164
  }
147
165
  ]"
148
166
  @click="selectSession(session)"
@@ -152,6 +170,16 @@
152
170
  role="button"
153
171
  :aria-current="activeSessionExportKey === getSessionExportKey(session) ? 'true' : null">
154
172
  <div class="session-item-header">
173
+ <label v-if="sessionBatchSelectMode" class="session-batch-check" @click.stop :data-session-key="getSessionExportKey(session)">
174
+ <input
175
+ type="checkbox"
176
+ :checked="isSessionSelectedForBatch(session)"
177
+ :disabled="sessionsLoading || sessionDeletingSelected || sessionDeleting[getSessionExportKey(session)] || !isSessionBatchSelectable(session)"
178
+ @change="toggleSessionSelectionForBatch(session)"
179
+ @click.stop
180
+ :aria-label="t('sessions.batch.select')">
181
+ <span class="session-batch-check-mark"></span>
182
+ </label>
155
183
  <div class="session-item-main">
156
184
  <div class="session-item-title">{{ session.title || session.sessionId }}</div>
157
185
  <span class="session-count-badge">{{ session.messageCount == null ? '...' : session.messageCount }}</span>
@@ -3292,7 +3292,63 @@ return function render(_ctx, _cache) {
3292
3292
  type: "button",
3293
3293
  onClick: _ctx.clearSessionFilters,
3294
3294
  disabled: _ctx.sessionsLoading
3295
- }, _toDisplayString(_ctx.t('common.resetFilters')), 9 /* TEXT, PROPS */, ["onClick", "disabled"])
3295
+ }, _toDisplayString(_ctx.t('common.resetFilters')), 9 /* TEXT, PROPS */, ["onClick", "disabled"]),
3296
+ (!_ctx.sessionBatchSelectMode)
3297
+ ? (_openBlock(), _createElementBlock("button", {
3298
+ key: 0,
3299
+ class: "btn-tool btn-tool-compact btn-tool-batch",
3300
+ type: "button",
3301
+ onClick: _ctx.enterSessionBatchSelectMode,
3302
+ disabled: _ctx.sessionsLoading,
3303
+ title: _ctx.t('sessions.batch.select')
3304
+ }, [
3305
+ (_openBlock(), _createElementBlock("svg", {
3306
+ viewBox: "0 0 24 24",
3307
+ fill: "none",
3308
+ stroke: "currentColor",
3309
+ "stroke-width": "2"
3310
+ }, [
3311
+ _createElementVNode("polyline", { points: "3 6 5 6 21 6" }),
3312
+ _createElementVNode("path", { d: "M7 6V4a2 2 0 012-2h6a2 2 0 012 2v2M10 11v6M14 11v6" }),
3313
+ _createElementVNode("path", { d: "M5 6l1 14a2 2 0 002 2h8a2 2 0 002-2l1-14" })
3314
+ ])),
3315
+ _createElementVNode("span", null, _toDisplayString(_ctx.t('sessions.batch.select')), 1 /* TEXT */)
3316
+ ], 8 /* PROPS */, ["onClick", "disabled", "title"]))
3317
+ : (_openBlock(), _createElementBlock(_Fragment, { key: 1 }, [
3318
+ _createElementVNode("button", {
3319
+ class: "btn-tool btn-tool-compact",
3320
+ type: "button",
3321
+ onClick: _ctx.toggleSelectAllVisibleForBatch,
3322
+ disabled: _ctx.sessionsLoading,
3323
+ title: _ctx.isAllVisibleBatchSelected() ? _ctx.t('sessions.batch.unselectAll') : _ctx.t('sessions.batch.selectAll')
3324
+ }, _toDisplayString(_ctx.isAllVisibleBatchSelected() ? _ctx.t('sessions.batch.unselectAll') : _ctx.t('sessions.batch.selectAll')), 9 /* TEXT, PROPS */, ["onClick", "disabled", "title"]),
3325
+ _createElementVNode("button", {
3326
+ class: "btn-tool btn-tool-compact btn-tool-batch-delete",
3327
+ type: "button",
3328
+ onClick: _ctx.deleteSelectedSessions,
3329
+ disabled: _ctx.sessionsLoading || _ctx.sessionDeletingSelected || _ctx.getSelectedBatchCount() === 0,
3330
+ title: _ctx.getSelectedBatchCount() > 0 ? _ctx.t('sessions.batch.deleteSelectedN', { count: _ctx.getSelectedBatchCount() }) : _ctx.t('sessions.batch.emptySelection')
3331
+ }, [
3332
+ (_openBlock(), _createElementBlock("svg", {
3333
+ viewBox: "0 0 24 24",
3334
+ fill: "none",
3335
+ stroke: "currentColor",
3336
+ "stroke-width": "2"
3337
+ }, [
3338
+ _createElementVNode("polyline", { points: "3 6 5 6 21 6" }),
3339
+ _createElementVNode("path", { d: "M7 6V4a2 2 0 012-2h6a2 2 0 012 2v2M10 11v6M14 11v6" }),
3340
+ _createElementVNode("path", { d: "M5 6l1 14a2 2 0 002 2h8a2 2 0 002-2l1-14" })
3341
+ ])),
3342
+ _createElementVNode("span", null, _toDisplayString(_ctx.sessionDeletingSelected ? _ctx.t('sessions.batch.deletingSelected') : (_ctx.getSelectedBatchCount() > 0 ? _ctx.t('sessions.batch.deleteSelectedN', { count: _ctx.getSelectedBatchCount() }) : _ctx.t('sessions.batch.deleteSelected'))), 1 /* TEXT */)
3343
+ ], 8 /* PROPS */, ["onClick", "disabled", "title"]),
3344
+ _createElementVNode("button", {
3345
+ class: "btn-tool btn-tool-compact",
3346
+ type: "button",
3347
+ onClick: _ctx.exitSessionBatchSelectMode,
3348
+ disabled: _ctx.sessionDeletingSelected,
3349
+ title: _ctx.t('sessions.batch.exit')
3350
+ }, _toDisplayString(_ctx.t('sessions.batch.exit')), 9 /* TEXT, PROPS */, ["onClick", "disabled", "title"])
3351
+ ], 64 /* STABLE_FRAGMENT */))
3296
3352
  ])
3297
3353
  ]),
3298
3354
  (_ctx.hasActiveSessionFilters())
@@ -3339,7 +3395,7 @@ return function render(_ctx, _cache) {
3339
3395
  onScrollPassive: _ctx.onSessionListScroll
3340
3396
  }, [
3341
3397
  (_openBlock(true), _createElementBlock(_Fragment, null, _renderList(_ctx.visibleSessionsList, (session, __, ___, _cached) => {
3342
- const _memo = ([_ctx.activeSessionExportKey === _ctx.getSessionExportKey(session), session.messageCount, session.updatedAt, session.title, session.sourceLabel, session.cwd, _ctx.isSessionPinned(session), _ctx.sessionsLoading, session.match && session.match.count])
3398
+ const _memo = ([_ctx.activeSessionExportKey === _ctx.getSessionExportKey(session), session.messageCount, session.updatedAt, session.title, session.sourceLabel, session.cwd, _ctx.isSessionPinned(session), _ctx.sessionsLoading, session.match && session.match.count, _ctx.sessionBatchSelectMode, _ctx.isSessionSelectedForBatch(session)])
3343
3399
  if (_cached && _cached.el && _cached.key === session.source + '-' + session.sessionId + '-' + session.filePath && _isMemoSame(_cached, _memo)) return _cached
3344
3400
  const _item = (_openBlock(), _createElementBlock("div", {
3345
3401
  key: session.source + '-' + session.sessionId + '-' + session.filePath,
@@ -3347,7 +3403,9 @@ return function render(_ctx, _cache) {
3347
3403
  'session-item',
3348
3404
  {
3349
3405
  active: _ctx.activeSessionExportKey === _ctx.getSessionExportKey(session),
3350
- pinned: _ctx.isSessionPinned(session)
3406
+ pinned: _ctx.isSessionPinned(session),
3407
+ 'batch-selected': _ctx.sessionBatchSelectMode && _ctx.isSessionSelectedForBatch(session),
3408
+ 'batch-selectable': _ctx.sessionBatchSelectMode
3351
3409
  }
3352
3410
  ]),
3353
3411
  onClick: $event => (_ctx.selectSession(session)),
@@ -3360,6 +3418,24 @@ return function render(_ctx, _cache) {
3360
3418
  "aria-current": _ctx.activeSessionExportKey === _ctx.getSessionExportKey(session) ? 'true' : null
3361
3419
  }, [
3362
3420
  _createElementVNode("div", { class: "session-item-header" }, [
3421
+ (_ctx.sessionBatchSelectMode)
3422
+ ? (_openBlock(), _createElementBlock("label", {
3423
+ key: 0,
3424
+ class: "session-batch-check",
3425
+ onClick: _withModifiers(() => {}, ["stop"]),
3426
+ "data-session-key": _ctx.getSessionExportKey(session)
3427
+ }, [
3428
+ _createElementVNode("input", {
3429
+ type: "checkbox",
3430
+ checked: _ctx.isSessionSelectedForBatch(session),
3431
+ disabled: _ctx.sessionsLoading || _ctx.sessionDeletingSelected || _ctx.sessionDeleting[_ctx.getSessionExportKey(session)] || !_ctx.isSessionBatchSelectable(session),
3432
+ onChange: $event => (_ctx.toggleSessionSelectionForBatch(session)),
3433
+ onClick: _withModifiers(() => {}, ["stop"]),
3434
+ "aria-label": _ctx.t('sessions.batch.select')
3435
+ }, null, 40 /* PROPS, NEED_HYDRATION */, ["checked", "disabled", "onChange", "onClick", "aria-label"]),
3436
+ _createElementVNode("span", { class: "session-batch-check-mark" })
3437
+ ], 8 /* PROPS */, ["onClick", "data-session-key"]))
3438
+ : _createCommentVNode("v-if", true),
3363
3439
  _createElementVNode("div", { class: "session-item-main" }, [
3364
3440
  _createElementVNode("div", { class: "session-item-title" }, _toDisplayString(session.title || session.sessionId), 1 /* TEXT */),
3365
3441
  _createElementVNode("span", { class: "session-count-badge" }, _toDisplayString(session.messageCount == null ? '...' : session.messageCount), 1 /* TEXT */),
@@ -276,6 +276,9 @@ export async function loadSessions(api, options = {}) {
276
276
  loadSucceeded = true;
277
277
  const rawSessions = Array.isArray(res.sessions) ? res.sessions : [];
278
278
  this.sessionsList = rawSessions.filter(s => s && typeof s === 'object');
279
+ if (typeof this.clearSessionBatchSelection === 'function') {
280
+ this.clearSessionBatchSelection();
281
+ }
279
282
  for (const session of this.sessionsList) {
280
283
  const rawUpdatedAt = typeof session.updatedAt === 'string' ? session.updatedAt : '';
281
284
  session.updatedAtLabel = formatSessionTimelineTimestamp(rawUpdatedAt);
@@ -687,3 +687,99 @@
687
687
  0%, 100% { opacity: 1; }
688
688
  50% { opacity: 0.6; }
689
689
  }
690
+
691
+ /* === 会话批量删除交互 === */
692
+ .session-item.batch-selectable {
693
+ cursor: default;
694
+ }
695
+
696
+ .session-item.batch-selectable .session-item-main,
697
+ .session-item.batch-selectable .session-item-meta {
698
+ pointer-events: none;
699
+ }
700
+
701
+ .session-item.batch-selectable .session-item-actions {
702
+ pointer-events: none;
703
+ opacity: 0.45;
704
+ }
705
+
706
+ .session-item.batch-selected {
707
+ background: var(--color-brand-light);
708
+ box-shadow: inset 3px 0 0 var(--color-brand);
709
+ }
710
+
711
+ .session-batch-check {
712
+ flex: 0 0 auto;
713
+ display: inline-flex;
714
+ align-items: center;
715
+ justify-content: center;
716
+ width: 22px;
717
+ height: 22px;
718
+ margin-right: 10px;
719
+ position: relative;
720
+ cursor: pointer;
721
+ user-select: none;
722
+ pointer-events: auto;
723
+ }
724
+
725
+ .session-batch-check input {
726
+ position: absolute;
727
+ inset: 0;
728
+ opacity: 0;
729
+ margin: 0;
730
+ cursor: pointer;
731
+ }
732
+
733
+ .session-batch-check-mark {
734
+ display: block;
735
+ width: 18px;
736
+ height: 18px;
737
+ border-radius: 6px;
738
+ border: 1.5px solid var(--color-border-strong);
739
+ background: var(--color-surface-elevated);
740
+ transition: background var(--transition-fast) var(--ease-spring-soft),
741
+ border-color var(--transition-fast) var(--ease-spring-soft);
742
+ }
743
+
744
+ .session-batch-check input:checked + .session-batch-check-mark {
745
+ background: var(--color-brand);
746
+ border-color: var(--color-brand);
747
+ }
748
+
749
+ .session-batch-check input:checked + .session-batch-check-mark::after {
750
+ content: '';
751
+ position: absolute;
752
+ left: 6px;
753
+ top: 3px;
754
+ width: 5px;
755
+ height: 9px;
756
+ border: solid #fff;
757
+ border-width: 0 2px 2px 0;
758
+ transform: rotate(45deg);
759
+ }
760
+
761
+ .session-batch-check input:disabled + .session-batch-check-mark {
762
+ opacity: 0.4;
763
+ cursor: not-allowed;
764
+ }
765
+
766
+ .session-item.batch-selectable .session-item-header {
767
+ align-items: center;
768
+ }
769
+
770
+ .btn-tool-batch,
771
+ .btn-tool-batch-delete {
772
+ display: inline-flex;
773
+ align-items: center;
774
+ gap: 5px;
775
+ }
776
+
777
+ .btn-tool-batch svg,
778
+ .btn-tool-batch-delete svg {
779
+ width: 14px;
780
+ height: 14px;
781
+ }
782
+
783
+ .btn-tool-batch-delete:not(:disabled) {
784
+ color: var(--color-error);
785
+ }