codexmate 0.1.7 → 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.
@@ -0,0 +1,177 @@
1
+ 'use strict';
2
+
3
+ function createSystemPromptFileController(deps = {}) {
4
+ const {
5
+ fs,
6
+ path,
7
+ os,
8
+ crypto,
9
+ buildLineDiff,
10
+ CONFIG_DIR,
11
+ PI_AGENT_DIR
12
+ } = deps;
13
+
14
+ if (!fs) throw new Error('createSystemPromptFileController 缺少 fs');
15
+ if (!path) throw new Error('createSystemPromptFileController 缺少 path');
16
+ if (!os) throw new Error('createSystemPromptFileController 缺少 os');
17
+ if (!crypto) throw new Error('createSystemPromptFileController 缺少 crypto');
18
+ if (typeof buildLineDiff !== 'function') throw new Error('createSystemPromptFileController 缺少 buildLineDiff');
19
+ if (typeof CONFIG_DIR !== 'string' || !CONFIG_DIR) throw new Error('createSystemPromptFileController 缺少 CONFIG_DIR');
20
+ if (typeof PI_AGENT_DIR !== 'string' || !PI_AGENT_DIR) throw new Error('createSystemPromptFileController 缺少 PI_AGENT_DIR');
21
+
22
+ const MODES = {
23
+ system: 'SYSTEM.md',
24
+ append: 'APPEND_SYSTEM.md'
25
+ };
26
+
27
+ const SCOPES = ['global', 'project'];
28
+
29
+ function normalizeMode(mode) {
30
+ const key = typeof mode === 'string' ? mode.trim() : '';
31
+ if (!Object.prototype.hasOwnProperty.call(MODES, key)) {
32
+ throw new Error("Invalid system prompt mode: expected 'system' or 'append'");
33
+ }
34
+ return key;
35
+ }
36
+
37
+ function normalizeScope(scope) {
38
+ const key = typeof scope === 'string' ? scope.trim() : '';
39
+ if (!SCOPES.includes(key)) {
40
+ throw new Error("Invalid system prompt scope: expected 'global' or 'project'");
41
+ }
42
+ return key;
43
+ }
44
+
45
+ function hashContent(content) {
46
+ return crypto.createHash('sha256').update(String(content || '')).digest('hex');
47
+ }
48
+
49
+ function resolveSystemPromptFilePath(params = {}) {
50
+ const scope = normalizeScope(params.scope);
51
+ const mode = normalizeMode(params.mode);
52
+ const filename = MODES[mode];
53
+ let base;
54
+ if (scope === 'global') {
55
+ base = PI_AGENT_DIR;
56
+ } else {
57
+ const cwd = typeof params.cwd === 'string' && params.cwd.trim()
58
+ ? params.cwd.trim()
59
+ : process.cwd();
60
+ base = path.join(path.resolve(cwd), '.pi');
61
+ }
62
+ return {
63
+ scope,
64
+ mode,
65
+ filename,
66
+ path: path.join(base, filename),
67
+ replaceDefault: mode === 'system'
68
+ };
69
+ }
70
+
71
+ function readSystemPromptFile(params = {}) {
72
+ const target = resolveSystemPromptFilePath(params);
73
+ const exists = fs.existsSync(target.path);
74
+ let content = '';
75
+ if (exists) {
76
+ try {
77
+ content = fs.readFileSync(target.path, 'utf8');
78
+ } catch (e) {
79
+ return { ...target, exists: false, content: '', hash: hashContent(''), error: '读取 system prompt 失败: ' + e.message };
80
+ }
81
+ }
82
+ return {
83
+ ...target,
84
+ exists,
85
+ content,
86
+ hash: hashContent(content)
87
+ };
88
+ }
89
+
90
+ function saveSystemPromptFile(params = {}) {
91
+ const content = typeof params.content === 'string' ? params.content : '';
92
+ if (!content.trim()) {
93
+ return { error: 'System prompt 不能为空' };
94
+ }
95
+ if (content.length > 2 * 1024 * 1024) {
96
+ return { error: '内容过大(最大 2MB)' };
97
+ }
98
+ const target = resolveSystemPromptFilePath(params);
99
+ const current = readSystemPromptFilePath(target);
100
+ if (current.error) {
101
+ return { error: current.error };
102
+ }
103
+ const baseHash = typeof params.baseHash === 'string' ? params.baseHash.trim() : '';
104
+ if (baseHash && baseHash !== current.hash) {
105
+ return { error: '文件已被外部修改,请重新加载后再保存' };
106
+ }
107
+ try {
108
+ const dir = path.dirname(target.path);
109
+ fs.mkdirSync(dir, { recursive: true });
110
+ const finalContent = content.endsWith('\n') ? content : content + '\n';
111
+ fs.writeFileSync(target.path, finalContent, { encoding: 'utf8', mode: 0o600 });
112
+ try { fs.chmodSync(target.path, 0o600); } catch (_) {}
113
+ } catch (e) {
114
+ return { error: '写入 system prompt 失败: ' + e.message };
115
+ }
116
+ return readSystemPromptFilePath(target);
117
+ }
118
+
119
+ function readSystemPromptFilePath(resolved) {
120
+ const exists = fs.existsSync(resolved.path);
121
+ let content = '';
122
+ if (exists) {
123
+ try {
124
+ content = fs.readFileSync(resolved.path, 'utf8');
125
+ } catch (e) {
126
+ return { ...resolved, exists: false, content: '', hash: hashContent(''), error: '读取 system prompt 失败: ' + e.message };
127
+ }
128
+ }
129
+ return {
130
+ ...resolved,
131
+ exists,
132
+ content,
133
+ hash: hashContent(content)
134
+ };
135
+ }
136
+
137
+ function buildSystemPromptDiff(params = {}) {
138
+ const hasBaseContent = typeof params.baseContent === 'string';
139
+ const target = resolveSystemPromptFilePath(params);
140
+ const current = readSystemPromptFilePath(target);
141
+ if (current.error) {
142
+ return { error: current.error };
143
+ }
144
+ const beforeText = hasBaseContent
145
+ ? (typeof params.baseContent === 'string' ? params.baseContent : '')
146
+ : (current.content || '');
147
+ const afterText = typeof params.content === 'string' ? params.content : '';
148
+ const normalizedBefore = beforeText.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
149
+ const normalizedAfter = afterText.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
150
+ const diff = buildLineDiff(normalizedBefore, normalizedAfter);
151
+ const hasChanges = diff.truncated
152
+ ? normalizedBefore !== normalizedAfter
153
+ : (diff.stats.added > 0 || diff.stats.removed > 0);
154
+ return {
155
+ diff: {
156
+ ...diff,
157
+ hasChanges
158
+ },
159
+ path: target.path,
160
+ exists: current.exists,
161
+ scope: target.scope,
162
+ mode: target.mode
163
+ };
164
+ }
165
+
166
+ return {
167
+ readSystemPromptFile,
168
+ saveSystemPromptFile,
169
+ buildSystemPromptDiff,
170
+ normalizeMode,
171
+ normalizeScope
172
+ };
173
+ }
174
+
175
+ module.exports = {
176
+ createSystemPromptFileController
177
+ };
package/cli.js CHANGED
@@ -126,6 +126,9 @@ const {
126
126
  const {
127
127
  createAgentsFileController
128
128
  } = require('./cli/agents-files');
129
+ const {
130
+ createSystemPromptFileController
131
+ } = require('./cli/system-prompt-files');
129
132
  const {
130
133
  createArchiveHelperController
131
134
  } = require('./cli/archive-helpers');
@@ -308,6 +311,7 @@ const FAST_SESSION_DETAIL_PREVIEW_FILE_BYTES = 256 * 1024;
308
311
  const FAST_SESSION_DETAIL_PREVIEW_CHUNK_BYTES = 64 * 1024;
309
312
  const FAST_SESSION_DETAIL_PREVIEW_MAX_BYTES = 1024 * 1024;
310
313
  const AGENTS_FILE_NAME = 'AGENTS.md';
314
+ const PI_AGENT_DIR = path.join(os.homedir(), '.pi', 'agent');
311
315
  const MODELS_CACHE_TTL_MS = 60 * 1000;
312
316
  const MODELS_NEGATIVE_CACHE_TTL_MS = 5 * 1000;
313
317
  const MODELS_CACHE_MAX_ENTRIES = 50;
@@ -2102,6 +2106,20 @@ const {
2102
2106
  }
2103
2107
  });
2104
2108
 
2109
+ const {
2110
+ readSystemPromptFile,
2111
+ saveSystemPromptFile,
2112
+ buildSystemPromptDiff
2113
+ } = createSystemPromptFileController({
2114
+ fs,
2115
+ path,
2116
+ os,
2117
+ crypto,
2118
+ buildLineDiff,
2119
+ CONFIG_DIR,
2120
+ PI_AGENT_DIR
2121
+ });
2122
+
2105
2123
  const {
2106
2124
  readOpenclawConfigFile,
2107
2125
  applyOpenclawConfig,
@@ -13479,6 +13497,15 @@ function createWebServer({ htmlPath, assetsDir, webDir, host, port, openBrowser
13479
13497
  case 'apply-openclaw-workspace-file':
13480
13498
  result = applyOpenclawWorkspaceFile(params || {});
13481
13499
  break;
13500
+ case 'get-system-prompt':
13501
+ result = readSystemPromptFile(params || {});
13502
+ break;
13503
+ case 'apply-system-prompt':
13504
+ result = saveSystemPromptFile(params || {});
13505
+ break;
13506
+ case 'preview-system-prompt-diff':
13507
+ result = buildSystemPromptDiff(params || {});
13508
+ break;
13482
13509
  case 'switch':
13483
13510
  case 'use':
13484
13511
  case 'add':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codexmate",
3
- "version": "0.1.7",
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
@@ -154,6 +154,25 @@ document.addEventListener('DOMContentLoaded', () => {
154
154
  agentsContext: 'codex',
155
155
  agentsModalTitle: 'AGENTS.md 编辑器',
156
156
  agentsModalHint: '保存后会写入目标 AGENTS.md(与 config.toml 同级)。',
157
+ sysPromptScope: 'global',
158
+ sysPromptMode: 'append',
159
+ sysPromptContent: '',
160
+ sysPromptOriginalContent: '',
161
+ sysPromptPath: '',
162
+ sysPromptExists: false,
163
+ sysPromptHash: '',
164
+ sysPromptLoading: false,
165
+ sysPromptSaving: false,
166
+ sysPromptDiffVisible: false,
167
+ sysPromptDiffLoading: false,
168
+ sysPromptDiffError: '',
169
+ sysPromptDiffLines: [],
170
+ sysPromptDiffStats: { added: 0, removed: 0, unchanged: 0 },
171
+ sysPromptDiffTruncated: false,
172
+ sysPromptDiffHasChangesValue: false,
173
+ sysPromptDiffFingerprint: '',
174
+ _sysPromptDiffPreviewRequestToken: null,
175
+ _sysPromptOpenRequestToken: null,
157
176
  skillsTargetApp: 'codex',
158
177
  skillsRootPath: '',
159
178
  skillsList: [],
@@ -221,6 +240,9 @@ document.addEventListener('DOMContentLoaded', () => {
221
240
  sessionImportingNative: {},
222
241
  sessionCloning: {},
223
242
  sessionDeleting: {},
243
+ sessionDeletingSelected: false,
244
+ sessionBatchSelectMode: false,
245
+ sessionSelectedKeys: {},
224
246
  activeSession: null,
225
247
  activeSessionMessages: [],
226
248
  activeSessionDetailError: '',
@@ -773,7 +773,7 @@ export function createSessionComputed() {
773
773
  const rows = currentKeys
774
774
  .map((key) => byDay.get(key))
775
775
  .filter(Boolean)
776
- .sort((a, b) => b.key.localeCompare(a.key, 'en-US'));
776
+ .sort((a, b) => a.key.localeCompare(b.key, 'en-US'));
777
777
  const rowsWithCompare = rows.map((row) => {
778
778
  if (!compareEnabled) {
779
779
  return { ...row, compareEnabled: false, prevKey: '', prevTokenTotal: 0 };
@@ -4,6 +4,7 @@ import {
4
4
  isAgentsDiffPreviewPayloadTooLarge,
5
5
  shouldApplyAgentsDiffPreviewResponse
6
6
  } from '../logic.mjs';
7
+ import { issueLatestRequestToken, isLatestRequestToken } from './request-token.mjs';
7
8
 
8
9
  function isValidOpenclawWorkspaceFileName(fileName) {
9
10
  if (typeof fileName !== 'string') {
@@ -19,16 +20,6 @@ function isValidOpenclawWorkspaceFileName(fileName) {
19
20
  return true;
20
21
  }
21
22
 
22
- function issueLatestRequestToken(context, key) {
23
- const token = (Number(context[key]) || 0) + 1;
24
- context[key] = token;
25
- return token;
26
- }
27
-
28
- function isLatestRequestToken(context, key, token) {
29
- return !!context && context[key] === token;
30
- }
31
-
32
23
  export function createAgentsMethods(options = {}) {
33
24
  const {
34
25
  api,
@@ -894,15 +885,17 @@ export function createAgentsMethods(options = {}) {
894
885
  this.showMessage(this.t('prompts.presets.toast.deleted'), 'success');
895
886
  },
896
887
  switchPromptsSubTab(subTab) {
897
- const normalized = subTab === 'claude-project' ? subTab : 'codex';
888
+ const normalized = subTab === 'claude-project' || subTab === 'system' ? subTab : 'codex';
898
889
  if (normalized === 'claude-project' && !this.projectPathOptions.length && !this.projectPathOptionsLoading) {
899
890
  this.loadProjectPathOptions();
900
891
  }
901
892
  if (this.promptsSubTab === normalized) {
902
- this.loadPromptsContent();
893
+ if (normalized === 'system') this.loadSystemPrompt();
894
+ else this.loadPromptsContent();
903
895
  return;
904
896
  }
905
897
  this.promptsSubTab = normalized;
898
+ if (normalized === 'system') this.loadSystemPrompt();
906
899
  this.$nextTick(() => {
907
900
  document.querySelector('.main-panel')?.scrollTo({ top: 0, left: 0, behavior: 'auto' });
908
901
  });
@@ -12,6 +12,7 @@ import {
12
12
  SESSION_TRASH_PAGE_SIZE
13
13
  } from './app.constants.mjs';
14
14
  import { createAgentsMethods } from './app.methods.agents.mjs';
15
+ import { createSystemPromptMethods } from './app.methods.system-prompt.mjs';
15
16
  import { createClaudeConfigMethods } from './app.methods.claude-config.mjs';
16
17
  import { createCodexConfigMethods } from './app.methods.codex-config.mjs';
17
18
  import { createInstallMethods } from './app.methods.install.mjs';
@@ -85,6 +86,7 @@ export function createAppMethods() {
85
86
  ...createSkillsMethods({ api }),
86
87
  ...createPluginsMethods(),
87
88
  ...createAgentsMethods({ api, apiWithMeta }),
89
+ ...createSystemPromptMethods({ api, apiWithMeta }),
88
90
  ...createProvidersMethods({ api }),
89
91
  ...createProviderCacheMethods({ api }),
90
92
  ...createWebUiPreferencesMethods({ api }),
@@ -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 = '';
@@ -783,6 +788,7 @@ export function createSessionBrowserMethods(options = {}) {
783
788
  this.sessionsUsageCompareEnabled = false;
784
789
  }
785
790
  void this.loadSessionsUsage({ range });
791
+ this.scrollSessionsUsageDayIntoView({ behavior: 'auto' });
786
792
  },
787
793
 
788
794
  toggleSessionsUsageCompare() {
@@ -801,12 +807,29 @@ export function createSessionBrowserMethods(options = {}) {
801
807
  selectSessionsUsageDay(dayKey) {
802
808
  const normalized = typeof dayKey === 'string' ? dayKey.trim() : '';
803
809
  this.sessionsUsageSelectedDayKey = normalized;
810
+ this.scrollSessionsUsageDayIntoView({ behavior: 'smooth', align: 'nearest' });
804
811
  },
805
812
 
806
813
  clearSessionsUsageDay() {
807
814
  this.sessionsUsageSelectedDayKey = '';
808
815
  },
809
816
 
817
+ scrollSessionsUsageDayIntoView(options = {}) {
818
+ if (typeof document === 'undefined' || !document) return;
819
+ const behavior = options.behavior === 'smooth' ? 'smooth' : 'auto';
820
+ const inline = options.align === 'nearest' ? 'nearest' : 'end';
821
+ const scrollFn = () => {
822
+ const node = document.querySelector('.usage-wave-label.active');
823
+ if (!node || typeof node.scrollIntoView !== 'function') return;
824
+ node.scrollIntoView({ inline, block: 'nearest', behavior });
825
+ };
826
+ if (typeof this.$nextTick === 'function') {
827
+ this.$nextTick(scrollFn);
828
+ } else {
829
+ scrollFn();
830
+ }
831
+ },
832
+
810
833
  async loadSessionsUsage(options = {}) {
811
834
  if (this.sessionsUsageLoading) return;
812
835
  const normalizedRange = typeof options.range === 'string'
@@ -855,7 +878,9 @@ export function createSessionBrowserMethods(options = {}) {
855
878
  this.sessionsUsageLoadedLimit = limit;
856
879
  this.sessionsUsageLastLoadedRange = range;
857
880
  if (!this.sessionsUsageSelectedDayKey && Array.isArray(this.sessionUsageDailyTableRows) && this.sessionUsageDailyTableRows.length > 0) {
858
- this.sessionsUsageSelectedDayKey = this.sessionUsageDailyTableRows[0].key;
881
+ const dayKeys = this.sessionUsageDailyTableRows.map((row) => row.key).filter(Boolean).sort((a, b) => b.localeCompare(a, 'en-US'));
882
+ this.sessionsUsageSelectedDayKey = dayKeys[0] || this.sessionUsageDailyTableRows[this.sessionUsageDailyTableRows.length - 1].key;
883
+ this.scrollSessionsUsageDayIntoView({ behavior: 'auto' });
859
884
  }
860
885
  }
861
886
  }