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.
- package/cli/system-prompt-files.js +177 -0
- package/cli.js +27 -0
- package/package.json +1 -1
- package/web-ui/app.js +22 -0
- package/web-ui/modules/app.computed.session.mjs +1 -1
- package/web-ui/modules/app.methods.agents.mjs +5 -12
- package/web-ui/modules/app.methods.index.mjs +2 -0
- package/web-ui/modules/app.methods.session-actions.mjs +161 -11
- package/web-ui/modules/app.methods.session-browser.mjs +26 -1
- package/web-ui/modules/app.methods.system-prompt.mjs +294 -0
- package/web-ui/modules/i18n/locales/en.mjs +24 -0
- package/web-ui/modules/i18n/locales/ja.mjs +24 -0
- package/web-ui/modules/i18n/locales/vi.mjs +24 -0
- package/web-ui/modules/i18n/locales/zh-tw.mjs +24 -0
- package/web-ui/modules/i18n/locales/zh.mjs +24 -0
- package/web-ui/modules/request-token.mjs +9 -0
- package/web-ui/partials/index/panel-prompts.html +96 -1
- package/web-ui/partials/index/panel-sessions.html +30 -2
- package/web-ui/res/web-ui-render.precompiled.js +633 -288
- package/web-ui/session-helpers.mjs +3 -0
- package/web-ui/styles/responsive.css +18 -0
- package/web-ui/styles/sessions-list.css +96 -0
- package/web-ui/styles/sessions-usage.css +20 -1
- package/web-ui/styles/system-prompt.css +23 -0
- package/web-ui/styles.css +1 -0
|
@@ -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
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) =>
|
|
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.
|
|
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
|
-
|
|
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 = '';
|
|
@@ -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
|
-
|
|
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
|
}
|