codexmate 0.0.18 → 0.0.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.en.md +34 -17
  2. package/README.md +34 -25
  3. package/cli/config-health.js +338 -0
  4. package/cli.js +1570 -839
  5. package/lib/cli-models-utils.js +186 -27
  6. package/lib/cli-network-utils.js +117 -101
  7. package/package.json +8 -1
  8. package/web-ui/app.js +379 -5754
  9. package/web-ui/index.html +15 -2079
  10. package/web-ui/logic.agents-diff.mjs +386 -0
  11. package/web-ui/logic.claude.mjs +108 -0
  12. package/web-ui/logic.mjs +5 -793
  13. package/web-ui/logic.runtime.mjs +124 -0
  14. package/web-ui/logic.sessions.mjs +263 -0
  15. package/web-ui/modules/api.mjs +69 -0
  16. package/web-ui/modules/app.computed.dashboard.mjs +113 -0
  17. package/web-ui/modules/app.computed.index.mjs +13 -0
  18. package/web-ui/modules/app.computed.session.mjs +141 -0
  19. package/web-ui/modules/app.constants.mjs +15 -0
  20. package/web-ui/modules/app.methods.agents.mjs +493 -0
  21. package/web-ui/modules/app.methods.claude-config.mjs +174 -0
  22. package/web-ui/modules/app.methods.codex-config.mjs +640 -0
  23. package/web-ui/modules/app.methods.index.mjs +86 -0
  24. package/web-ui/modules/app.methods.install.mjs +157 -0
  25. package/web-ui/modules/app.methods.navigation.mjs +478 -0
  26. package/web-ui/modules/app.methods.openclaw-core.mjs +514 -0
  27. package/web-ui/modules/app.methods.openclaw-editing.mjs +337 -0
  28. package/web-ui/modules/app.methods.openclaw-persist.mjs +251 -0
  29. package/web-ui/modules/app.methods.providers.mjs +265 -0
  30. package/web-ui/modules/app.methods.runtime.mjs +323 -0
  31. package/web-ui/modules/app.methods.session-actions.mjs +457 -0
  32. package/web-ui/modules/app.methods.session-browser.mjs +435 -0
  33. package/web-ui/modules/app.methods.session-timeline.mjs +441 -0
  34. package/web-ui/modules/app.methods.session-trash.mjs +419 -0
  35. package/web-ui/modules/app.methods.startup-claude.mjs +406 -0
  36. package/web-ui/modules/config-mode.computed.mjs +1 -0
  37. package/web-ui/modules/skills.computed.mjs +26 -1
  38. package/web-ui/modules/skills.methods.mjs +154 -23
  39. package/web-ui/partials/index/layout-footer.html +69 -0
  40. package/web-ui/partials/index/layout-header.html +337 -0
  41. package/web-ui/partials/index/modal-config-template-agents.html +125 -0
  42. package/web-ui/partials/index/modal-confirm-toast.html +32 -0
  43. package/web-ui/partials/index/modal-health-check.html +72 -0
  44. package/web-ui/partials/index/modal-openclaw-config.html +275 -0
  45. package/web-ui/partials/index/modal-skills.html +184 -0
  46. package/web-ui/partials/index/modals-basic.html +196 -0
  47. package/web-ui/partials/index/panel-config-claude.html +100 -0
  48. package/web-ui/partials/index/panel-config-codex.html +237 -0
  49. package/web-ui/partials/index/panel-config-openclaw.html +84 -0
  50. package/web-ui/partials/index/panel-market.html +174 -0
  51. package/web-ui/partials/index/panel-sessions.html +387 -0
  52. package/web-ui/partials/index/panel-settings.html +166 -0
  53. package/web-ui/session-helpers.mjs +12 -0
  54. package/web-ui/source-bundle.cjs +233 -0
  55. package/web-ui/styles/base-theme.css +373 -0
  56. package/web-ui/styles/controls-forms.css +354 -0
  57. package/web-ui/styles/feedback.css +108 -0
  58. package/web-ui/styles/health-check-dialog.css +144 -0
  59. package/web-ui/styles/layout-shell.css +330 -0
  60. package/web-ui/styles/modals-core.css +449 -0
  61. package/web-ui/styles/navigation-panels.css +381 -0
  62. package/web-ui/styles/openclaw-structured.css +266 -0
  63. package/web-ui/styles/responsive.css +416 -0
  64. package/web-ui/styles/sessions-list.css +414 -0
  65. package/web-ui/styles/sessions-preview.css +405 -0
  66. package/web-ui/styles/sessions-toolbar-trash.css +243 -0
  67. package/web-ui/styles/sessions-usage.css +276 -0
  68. package/web-ui/styles/skills-list.css +298 -0
  69. package/web-ui/styles/skills-market.css +335 -0
  70. package/web-ui/styles/titles-cards.css +407 -0
  71. package/web-ui/styles.css +16 -4499
  72. package/doc/CHANGELOG.md +0 -32
  73. package/doc/CHANGELOG.zh-CN.md +0 -34
@@ -0,0 +1,124 @@
1
+ export function formatLatency(result) {
2
+ if (!result) return '';
3
+ if (!result.ok) return result.status ? `ERR ${result.status}` : 'ERR';
4
+ const ms = (typeof result.durationMs === 'number' && Number.isFinite(result.durationMs))
5
+ ? result.durationMs
6
+ : 0;
7
+ return `${ms}ms`;
8
+ }
9
+
10
+ export function buildSpeedTestIssue(name, result) {
11
+ if (!name || !result) return null;
12
+ if (result.error) {
13
+ const error = String(result.error || '');
14
+ const errorLower = error.toLowerCase();
15
+ if (error === 'Provider not found') {
16
+ return {
17
+ code: 'remote-speedtest-provider-missing',
18
+ message: `提供商 ${name} 未找到,无法测速`,
19
+ suggestion: '检查配置是否存在该 provider'
20
+ };
21
+ }
22
+ if (error === 'Provider missing URL' || error === 'Missing name or url') {
23
+ return {
24
+ code: 'remote-speedtest-baseurl-missing',
25
+ message: `提供商 ${name} 缺少 base_url`,
26
+ suggestion: '补全 base_url 后重试'
27
+ };
28
+ }
29
+ if (errorLower.includes('invalid url')) {
30
+ return {
31
+ code: 'remote-speedtest-invalid-url',
32
+ message: `提供商 ${name} 的 base_url 无效`,
33
+ suggestion: '请设置为 http/https 的完整 URL'
34
+ };
35
+ }
36
+ if (errorLower.includes('timeout')) {
37
+ return {
38
+ code: 'remote-speedtest-timeout',
39
+ message: `提供商 ${name} 远程测速超时`,
40
+ suggestion: '检查网络或 base_url 是否可达'
41
+ };
42
+ }
43
+ return {
44
+ code: 'remote-speedtest-unreachable',
45
+ message: `提供商 ${name} 远程测速失败:${error || '无法连接'}`,
46
+ suggestion: '检查网络或 base_url 是否可用'
47
+ };
48
+ }
49
+
50
+ const status = typeof result.status === 'number' ? result.status : 0;
51
+ if (status === 401 || status === 403) {
52
+ return {
53
+ code: 'remote-speedtest-auth-failed',
54
+ message: `提供商 ${name} 远程测速鉴权失败(401/403)`,
55
+ suggestion: '检查 API Key 或认证方式'
56
+ };
57
+ }
58
+ if (status >= 400) {
59
+ return {
60
+ code: 'remote-speedtest-http-error',
61
+ message: `提供商 ${name} 远程测速返回异常状态: ${status}`,
62
+ suggestion: '检查 base_url 或服务状态'
63
+ };
64
+ }
65
+ return null;
66
+ }
67
+
68
+ export async function runLatestOnlyQueue(initialTarget, options = {}) {
69
+ const perform = typeof options.perform === 'function'
70
+ ? options.perform
71
+ : async () => {};
72
+ const consumePending = typeof options.consumePending === 'function'
73
+ ? options.consumePending
74
+ : () => '';
75
+ let currentTarget = typeof initialTarget === 'string' ? initialTarget.trim() : '';
76
+ let lastError = '';
77
+
78
+ while (currentTarget) {
79
+ try {
80
+ await perform(currentTarget);
81
+ lastError = '';
82
+ } catch (e) {
83
+ lastError = e && e.message ? e.message : 'queue task failed';
84
+ }
85
+ const queued = String(consumePending() || '').trim();
86
+ if (!queued || queued === currentTarget) {
87
+ break;
88
+ }
89
+ currentTarget = queued;
90
+ }
91
+
92
+ return {
93
+ lastTarget: currentTarget,
94
+ lastError
95
+ };
96
+ }
97
+
98
+ export function shouldForceCompactLayoutMode(options = {}) {
99
+ const viewportWidth = Number(options.viewportWidth || 0);
100
+ const screenWidth = Number(options.screenWidth || 0);
101
+ const screenHeight = Number(options.screenHeight || 0);
102
+ const shortEdge = Number(options.shortEdge || (screenWidth > 0 && screenHeight > 0 ? Math.min(screenWidth, screenHeight) : 0));
103
+ const maxTouchPoints = Number(options.maxTouchPoints || 0);
104
+ const userAgent = typeof options.userAgent === 'string' ? options.userAgent : '';
105
+ const isMobileUa = typeof options.isMobileUa === 'boolean'
106
+ ? options.isMobileUa
107
+ : /(Android|iPhone|iPad|iPod|Mobile)/i.test(userAgent);
108
+ const coarsePointer = !!options.coarsePointer;
109
+ const noHover = !!options.noHover;
110
+ const isSmallPhysicalScreen = shortEdge > 0 && shortEdge <= 920;
111
+ const isNarrowViewport = viewportWidth > 0 && viewportWidth <= 960;
112
+ const pointerSuggestsTouchOnly = coarsePointer && noHover;
113
+
114
+ if (isMobileUa) {
115
+ return isNarrowViewport || isSmallPhysicalScreen;
116
+ }
117
+ if (!pointerSuggestsTouchOnly) {
118
+ return false;
119
+ }
120
+ if (maxTouchPoints <= 0) {
121
+ return false;
122
+ }
123
+ return isSmallPhysicalScreen;
124
+ }
@@ -0,0 +1,263 @@
1
+ export function isSessionQueryEnabled(source) {
2
+ const normalized = normalizeSessionSource(source, '');
3
+ return normalized === 'codex' || normalized === 'claude' || normalized === 'all';
4
+ }
5
+
6
+ export function normalizeSessionSource(source, fallback = 'all') {
7
+ const normalized = typeof source === 'string'
8
+ ? source.trim().toLowerCase()
9
+ : '';
10
+ if (normalized === 'codex' || normalized === 'claude' || normalized === 'all') {
11
+ return normalized;
12
+ }
13
+ return fallback;
14
+ }
15
+
16
+ export function normalizeSessionPathFilter(pathFilter) {
17
+ return typeof pathFilter === 'string' ? pathFilter.trim() : '';
18
+ }
19
+
20
+ export function buildSessionFilterCacheState(source, pathFilter) {
21
+ return {
22
+ source: normalizeSessionSource(source, 'all'),
23
+ pathFilter: normalizeSessionPathFilter(pathFilter)
24
+ };
25
+ }
26
+
27
+ export function buildSessionListParams(options = {}) {
28
+ const {
29
+ source = 'all',
30
+ pathFilter = '',
31
+ query = '',
32
+ roleFilter = 'all',
33
+ timeRangePreset = 'all',
34
+ limit = 200
35
+ } = options;
36
+ const normalizedSource = normalizeSessionSource(source, 'all');
37
+ const normalizedPathFilter = normalizeSessionPathFilter(pathFilter);
38
+ const queryValue = isSessionQueryEnabled(normalizedSource) ? query : '';
39
+ return {
40
+ source: normalizedSource,
41
+ pathFilter: normalizedPathFilter,
42
+ query: queryValue,
43
+ queryMode: 'and',
44
+ queryScope: 'content',
45
+ contentScanLimit: 50,
46
+ roleFilter,
47
+ timeRangePreset,
48
+ limit,
49
+ forceRefresh: true
50
+ };
51
+ }
52
+
53
+ export function normalizeSessionMessageRole(role) {
54
+ const value = typeof role === 'string' ? role.trim().toLowerCase() : '';
55
+ if (value === 'user' || value === 'assistant' || value === 'system') {
56
+ return value;
57
+ }
58
+ return 'assistant';
59
+ }
60
+
61
+ function toRoleMeta(role) {
62
+ if (role === 'user') {
63
+ return { role: 'user', roleLabel: 'User', roleShort: 'U' };
64
+ }
65
+ if (role === 'assistant') {
66
+ return { role: 'assistant', roleLabel: 'Assistant', roleShort: 'A' };
67
+ }
68
+ if (role === 'system') {
69
+ return { role: 'system', roleLabel: 'System', roleShort: 'S' };
70
+ }
71
+ return { role: 'mixed', roleLabel: 'Mixed', roleShort: 'M' };
72
+ }
73
+
74
+ function clampTimelinePercent(percent) {
75
+ return Math.max(6, Math.min(94, percent));
76
+ }
77
+
78
+ export function formatSessionTimelineTimestamp(timestamp) {
79
+ const value = typeof timestamp === 'string' ? timestamp.trim() : '';
80
+ if (!value) return '';
81
+
82
+ const matched = value.match(/^(\d{4})-(\d{2})-(\d{2})[T\s](\d{2}):(\d{2})(?::(\d{2}))?/);
83
+ if (matched) {
84
+ const second = matched[6] || '00';
85
+ return `${matched[2]}-${matched[3]} ${matched[4]}:${matched[5]}:${second}`;
86
+ }
87
+
88
+ return value;
89
+ }
90
+
91
+ export function buildUsageChartGroups(sessions = [], options = {}) {
92
+ const list = Array.isArray(sessions) ? sessions : [];
93
+ const range = typeof options.range === 'string' ? options.range.trim().toLowerCase() : '7d';
94
+ const now = Number.isFinite(Number(options.now)) ? Number(options.now) : Date.now();
95
+ const dayMs = 24 * 60 * 60 * 1000;
96
+ const rangeDays = range === '30d' ? 30 : 7;
97
+ const buckets = [];
98
+ for (let i = rangeDays - 1; i >= 0; i -= 1) {
99
+ const stamp = new Date(now - (i * dayMs));
100
+ const key = `${stamp.getUTCFullYear()}-${String(stamp.getUTCMonth() + 1).padStart(2, '0')}-${String(stamp.getUTCDate()).padStart(2, '0')}`;
101
+ buckets.push({
102
+ key,
103
+ label: key.slice(5),
104
+ codex: 0,
105
+ claude: 0,
106
+ totalMessages: 0,
107
+ totalSessions: 0
108
+ });
109
+ }
110
+ const bucketMap = new Map(buckets.map((bucket) => [bucket.key, bucket]));
111
+ let codexTotal = 0;
112
+ let claudeTotal = 0;
113
+ let messageTotal = 0;
114
+ const pathMap = new Map();
115
+
116
+ for (const session of list) {
117
+ if (!session || typeof session !== 'object') continue;
118
+ const source = normalizeSessionSource(session.source, '');
119
+ if (source !== 'codex' && source !== 'claude') continue;
120
+ const updatedAtMs = Date.parse(session.updatedAt || '');
121
+ if (!Number.isFinite(updatedAtMs)) continue;
122
+ const stamp = new Date(updatedAtMs);
123
+ const key = `${stamp.getUTCFullYear()}-${String(stamp.getUTCMonth() + 1).padStart(2, '0')}-${String(stamp.getUTCDate()).padStart(2, '0')}`;
124
+ const bucket = bucketMap.get(key);
125
+ if (!bucket) continue;
126
+ const messageCount = Number.isFinite(Number(session.messageCount))
127
+ ? Math.max(0, Math.floor(Number(session.messageCount)))
128
+ : 0;
129
+ bucket.totalSessions += 1;
130
+ bucket.totalMessages += messageCount;
131
+ if (source === 'codex') {
132
+ bucket.codex += 1;
133
+ codexTotal += 1;
134
+ } else {
135
+ bucket.claude += 1;
136
+ claudeTotal += 1;
137
+ }
138
+ messageTotal += messageCount;
139
+ const cwd = normalizeSessionPathFilter(session.cwd);
140
+ if (cwd) {
141
+ pathMap.set(cwd, (Number(pathMap.get(cwd)) || 0) + 1);
142
+ }
143
+ }
144
+
145
+ const totalSessions = codexTotal + claudeTotal;
146
+ const sourceShare = [
147
+ { key: 'codex', label: 'Codex', value: codexTotal },
148
+ { key: 'claude', label: 'Claude', value: claudeTotal }
149
+ ].map((item) => ({
150
+ ...item,
151
+ percent: totalSessions > 0 ? Math.round((item.value / totalSessions) * 100) : 0
152
+ }));
153
+
154
+ const topPaths = [...pathMap.entries()]
155
+ .sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0], 'zh-Hans-CN'))
156
+ .slice(0, 5)
157
+ .map(([pathValue, count]) => ({ path: pathValue, count }));
158
+
159
+ const maxSessionBucket = buckets.reduce((max, item) => Math.max(max, item.totalSessions), 0);
160
+ const maxMessageBucket = buckets.reduce((max, item) => Math.max(max, item.totalMessages), 0);
161
+
162
+ return {
163
+ range,
164
+ buckets,
165
+ summary: {
166
+ totalSessions,
167
+ totalMessages: messageTotal,
168
+ codexTotal,
169
+ claudeTotal,
170
+ activeDays: buckets.filter((item) => item.totalSessions > 0).length
171
+ },
172
+ sourceShare,
173
+ topPaths,
174
+ maxSessionBucket,
175
+ maxMessageBucket
176
+ };
177
+ }
178
+
179
+ export function buildSessionTimelineNodes(messages = [], options = {}) {
180
+ const list = Array.isArray(messages) ? messages : [];
181
+ const getKey = typeof options.getKey === 'function'
182
+ ? options.getKey
183
+ : ((_message, index) => `msg-${index}`);
184
+ const total = list.length;
185
+ const rawMaxMarkers = Number(options.maxMarkers);
186
+ const maxMarkers = Number.isFinite(rawMaxMarkers)
187
+ ? Math.max(1, Math.min(80, Math.floor(rawMaxMarkers)))
188
+ : 30;
189
+
190
+ const buildSingleNode = (message, index) => {
191
+ const role = normalizeSessionMessageRole(message && (message.normalizedRole || message.role));
192
+ const roleMeta = toRoleMeta(role);
193
+ const key = String(getKey(message, index) || `msg-${index}`);
194
+ const displayTime = formatSessionTimelineTimestamp(message && message.timestamp ? message.timestamp : '');
195
+ const title = displayTime
196
+ ? `#${index + 1} · ${roleMeta.roleLabel} · ${displayTime}`
197
+ : `#${index + 1} · ${roleMeta.roleLabel}`;
198
+ const percent = total <= 1 ? 0 : (index / (total - 1)) * 100;
199
+ return {
200
+ key,
201
+ role: roleMeta.role,
202
+ roleLabel: roleMeta.roleLabel,
203
+ roleShort: roleMeta.roleShort,
204
+ displayTime,
205
+ title,
206
+ percent,
207
+ safePercent: clampTimelinePercent(percent)
208
+ };
209
+ };
210
+
211
+ if (total <= maxMarkers) {
212
+ return list.map((message, index) => buildSingleNode(message, index));
213
+ }
214
+
215
+ const nodes = [];
216
+ const bucketWidth = total / maxMarkers;
217
+ for (let bucket = 0; bucket < maxMarkers; bucket += 1) {
218
+ let start = Math.floor(bucket * bucketWidth);
219
+ if (nodes.length && start <= nodes[nodes.length - 1].endIndex) {
220
+ start = nodes[nodes.length - 1].endIndex + 1;
221
+ }
222
+ if (start >= total) {
223
+ break;
224
+ }
225
+ let end = Math.floor((bucket + 1) * bucketWidth) - 1;
226
+ end = Math.max(start, Math.min(total - 1, end));
227
+ const targetIndex = Math.min(total - 1, start + Math.floor((end - start) / 2));
228
+ const targetMessage = list[targetIndex] || null;
229
+ const key = String(getKey(targetMessage, targetIndex) || `msg-${targetIndex}`);
230
+ const percent = total <= 1 ? 0 : (targetIndex / (total - 1)) * 100;
231
+ const messagesInGroup = end - start + 1;
232
+ const roleSet = new Set();
233
+ for (let i = start; i <= end; i += 1) {
234
+ roleSet.add(normalizeSessionMessageRole(list[i] && (list[i].normalizedRole || list[i].role)));
235
+ }
236
+ const roleValue = roleSet.size === 1 ? Array.from(roleSet)[0] : 'mixed';
237
+ const roleMeta = toRoleMeta(roleValue);
238
+ const firstTime = formatSessionTimelineTimestamp(list[start] && list[start].timestamp ? list[start].timestamp : '');
239
+ const lastTime = formatSessionTimelineTimestamp(list[end] && list[end].timestamp ? list[end].timestamp : '');
240
+ let displayTime = '';
241
+ if (firstTime && lastTime) {
242
+ displayTime = firstTime === lastTime ? firstTime : `${firstTime} ~ ${lastTime}`;
243
+ } else {
244
+ displayTime = firstTime || lastTime;
245
+ }
246
+ const titleBase = `#${start + 1}-${end + 1} · ${messagesInGroup} msgs · ${roleMeta.roleLabel}`;
247
+ const title = displayTime ? `${titleBase} · ${displayTime}` : titleBase;
248
+ nodes.push({
249
+ key,
250
+ role: roleMeta.role,
251
+ roleLabel: roleMeta.roleLabel,
252
+ roleShort: roleMeta.roleShort,
253
+ displayTime,
254
+ title,
255
+ percent,
256
+ safePercent: clampTimelinePercent(percent),
257
+ startIndex: start,
258
+ endIndex: end,
259
+ messageCount: messagesInGroup
260
+ });
261
+ }
262
+ return nodes;
263
+ }
@@ -0,0 +1,69 @@
1
+ const browserLocation = typeof location !== 'undefined' ? location : null;
2
+
3
+ export const API_BASE = (browserLocation && browserLocation.origin && browserLocation.origin !== 'null')
4
+ ? browserLocation.origin
5
+ : 'http://localhost:3737';
6
+
7
+ async function postApi(action, params = {}) {
8
+ return await fetch(`${API_BASE}/api`, {
9
+ method: 'POST',
10
+ headers: { 'Content-Type': 'application/json' },
11
+ body: JSON.stringify({ action, params })
12
+ });
13
+ }
14
+
15
+ function buildApiResponseContext(action, res, contentType) {
16
+ return `${action} (${res.status} ${res.statusText}, content-type: ${contentType || 'unknown'})`;
17
+ }
18
+
19
+ function withPayloadTooLargeErrorCode(res, payload) {
20
+ if (res.status !== 413 || (payload && typeof payload === 'object' && payload.errorCode)) {
21
+ return payload;
22
+ }
23
+ return { ...payload, errorCode: 'payload-too-large' };
24
+ }
25
+
26
+ export async function api(action, params = {}) {
27
+ const res = await postApi(action, params);
28
+ const contentType = String(res.headers.get('content-type') || '').toLowerCase();
29
+ if (contentType && !contentType.includes('application/json')) {
30
+ const body = await res.text();
31
+ const errorDetails = buildApiResponseContext(action, res, contentType);
32
+ const bodyDetails = body ? `: ${body}` : '';
33
+ throw new Error(`Unexpected non-JSON API response for ${errorDetails}${bodyDetails}`);
34
+ }
35
+ try {
36
+ return await res.json();
37
+ } catch (error) {
38
+ const errorDetails = buildApiResponseContext(action, res, contentType);
39
+ throw new Error(`Failed to parse API response for ${errorDetails}: ${error.message}`);
40
+ }
41
+ }
42
+
43
+ export async function apiWithMeta(action, params = {}) {
44
+ const res = await postApi(action, params);
45
+ const contentType = String(res.headers.get('content-type') || '').toLowerCase();
46
+ if (contentType.includes('application/json')) {
47
+ try {
48
+ const payload = await res.json();
49
+ if (payload && typeof payload === 'object' && !Array.isArray(payload)) {
50
+ return { ...withPayloadTooLargeErrorCode(res, payload), ok: res.ok, status: res.status };
51
+ }
52
+ return res.status === 413
53
+ ? { ok: res.ok, status: res.status, data: payload, errorCode: 'payload-too-large' }
54
+ : { ok: res.ok, status: res.status, data: payload };
55
+ } catch (error) {
56
+ if (res.status === 413) {
57
+ return { ok: false, status: 413, errorCode: 'payload-too-large' };
58
+ }
59
+ throw error;
60
+ }
61
+ }
62
+ const error = await res.text();
63
+ return {
64
+ ok: res.ok,
65
+ status: res.status,
66
+ error,
67
+ errorCode: res.status === 413 ? 'payload-too-large' : ''
68
+ };
69
+ }
@@ -0,0 +1,113 @@
1
+ export function createDashboardComputed() {
2
+ return {
3
+ agentsDiffHasChanges() {
4
+ if (this.agentsDiffTruncated) {
5
+ return !!this.agentsDiffHasChangesValue;
6
+ }
7
+ const stats = this.agentsDiffStats || {};
8
+ const added = Number(stats.added || 0);
9
+ const removed = Number(stats.removed || 0);
10
+ return added > 0 || removed > 0;
11
+ },
12
+ claudeModelHasList() {
13
+ return this.claudeModelOptions.length > 0;
14
+ },
15
+ claudeModelOptions() {
16
+ const list = Array.isArray(this.claudeModels) ? [...this.claudeModels] : [];
17
+ const current = (this.currentClaudeModel || '').trim();
18
+ if (current && !list.includes(current)) {
19
+ list.unshift(current);
20
+ }
21
+ return list;
22
+ },
23
+ hasLocalAndProxy() {
24
+ return false;
25
+ },
26
+ displayCurrentProvider() {
27
+ const switching = String(this.providerSwitchDisplayTarget || '').trim();
28
+ if (switching) return switching;
29
+ const current = String(this.currentProvider || '').trim();
30
+ return current;
31
+ },
32
+ displayProvidersList() {
33
+ const list = Array.isArray(this.providersList) ? this.providersList : [];
34
+ return list.filter((item) => String(item && item.name ? item.name : '').trim().toLowerCase() !== 'codexmate-proxy');
35
+ },
36
+ installTargetCards() {
37
+ const targets = Array.isArray(this.installStatusTargets) ? this.installStatusTargets : [];
38
+ const action = this.normalizeInstallAction(this.installCommandAction);
39
+ return targets.map((target) => {
40
+ const id = target && typeof target.id === 'string' ? target.id : '';
41
+ return {
42
+ ...target,
43
+ command: this.getInstallCommand(id, action)
44
+ };
45
+ });
46
+ },
47
+ installRegistryPreview() {
48
+ return this.resolveInstallRegistryUrl(this.installRegistryPreset, this.installRegistryCustom);
49
+ },
50
+ inspectorBusyStatus() {
51
+ const tasks = [];
52
+ if (this.loading) tasks.push('初始化');
53
+ if (this.sessionsLoading) tasks.push('会话加载');
54
+ if (this.codexModelsLoading || this.claudeModelsLoading) tasks.push('模型加载');
55
+ if (this.codexApplying || this.configTemplateApplying || this.openclawApplying) tasks.push('配置应用');
56
+ if (this.agentsSaving) tasks.push('AGENTS 保存');
57
+ if (this.skillsLoading || this.skillsDeleting || this.skillsScanningImports || this.skillsImporting || this.skillsZipImporting || this.skillsExporting) tasks.push('Skills 管理');
58
+ return tasks.length ? tasks.join(' / ') : '空闲';
59
+ },
60
+ inspectorMessageSummary() {
61
+ const value = typeof this.message === 'string' ? this.message.trim() : '';
62
+ return value || '暂无提示';
63
+ },
64
+ inspectorSessionSourceLabel() {
65
+ if (this.sessionFilterSource === 'codex') return 'Codex';
66
+ if (this.sessionFilterSource === 'claude') return 'Claude Code';
67
+ return '全部';
68
+ },
69
+ inspectorSessionPathLabel() {
70
+ const value = typeof this.sessionPathFilter === 'string' ? this.sessionPathFilter.trim() : '';
71
+ return value || '全部路径';
72
+ },
73
+ inspectorSessionQueryLabel() {
74
+ if (!this.isSessionQueryEnabled) return '当前来源不支持';
75
+ const value = typeof this.sessionQuery === 'string' ? this.sessionQuery.trim() : '';
76
+ return value || '未设置';
77
+ },
78
+ inspectorHealthStatus() {
79
+ if (this.initError) return '读取失败';
80
+ if (this.loading) return '初始化中';
81
+ return '正常';
82
+ },
83
+ inspectorHealthTone() {
84
+ if (this.initError) return 'error';
85
+ if (this.loading) return 'warn';
86
+ return 'ok';
87
+ },
88
+ inspectorModelLoadStatus() {
89
+ if (this.codexModelsLoading || this.claudeModelsLoading) {
90
+ return '加载中';
91
+ }
92
+ if (this.modelsSource === 'error' || this.claudeModelsSource === 'error') {
93
+ return '加载异常';
94
+ }
95
+ return '正常';
96
+ },
97
+ installTroubleshootingTips() {
98
+ const platform = this.resolveInstallPlatform();
99
+ if (platform === 'win32') {
100
+ return [
101
+ 'PowerShell 报权限不足(EACCES/EPERM)时,请以管理员身份执行安装命令。',
102
+ '安装后若仍提示找不到命令,重开终端并执行:where codex / where claude。',
103
+ '公司网络受限时,可先切换镜像源快捷项(npmmirror / 腾讯云 / 自定义)。'
104
+ ];
105
+ }
106
+ return [
107
+ '出现 EACCES 权限错误时,优先修复 Node 全局目录权限,不建议直接 sudo npm。',
108
+ '安装后若命令未生效,重开终端并执行:which codex / which claude。',
109
+ '公司网络受限时,可先切换镜像源快捷项(npmmirror / 腾讯云 / 自定义)。'
110
+ ];
111
+ }
112
+ };
113
+ }
@@ -0,0 +1,13 @@
1
+ import { createDashboardComputed } from './app.computed.dashboard.mjs';
2
+ import { createSessionComputed } from './app.computed.session.mjs';
3
+ import { createConfigModeComputed } from './config-mode.computed.mjs';
4
+ import { createSkillsComputed } from './skills.computed.mjs';
5
+
6
+ export function createAppComputed() {
7
+ return {
8
+ ...createSessionComputed(),
9
+ ...createDashboardComputed(),
10
+ ...createSkillsComputed(),
11
+ ...createConfigModeComputed()
12
+ };
13
+ }