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 +1 -1
- package/web-ui/app.js +3 -0
- package/web-ui/modules/app.methods.session-actions.mjs +161 -11
- package/web-ui/modules/app.methods.session-browser.mjs +5 -0
- package/web-ui/modules/i18n/locales/en.mjs +13 -0
- package/web-ui/modules/i18n/locales/ja.mjs +13 -0
- package/web-ui/modules/i18n/locales/vi.mjs +13 -0
- package/web-ui/modules/i18n/locales/zh-tw.mjs +13 -0
- package/web-ui/modules/i18n/locales/zh.mjs +13 -0
- package/web-ui/partials/index/panel-sessions.html +30 -2
- package/web-ui/res/web-ui-render.precompiled.js +79 -3
- package/web-ui/session-helpers.mjs +3 -0
- package/web-ui/styles/sessions-list.css +96 -0
package/package.json
CHANGED
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
|
-
|
|
674
|
-
|
|
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
|
-
|
|
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
|
+
}
|