ec-web 0.0.20 → 0.0.22

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,1576 @@
1
+ /* Basic Settings UI. This file deliberately owns only presentation state;
2
+ * persistence goes through /api/basic-settings and never replaces a whole file. */
3
+ (function () {
4
+ 'use strict';
5
+
6
+ const state = {
7
+ snapshot: null,
8
+ tab: 'models',
9
+ loading: false,
10
+ saving: false,
11
+ error: '',
12
+ openAgents: new Set(['claude']),
13
+ catalogs: Object.create(null),
14
+ changes: { daemon: new Map(), defaults: new Map() },
15
+ mapDrafts: new Map(),
16
+ serviceMetadataDrafts: new Map(),
17
+ restartPaths: [],
18
+ notice: '',
19
+ noticeType: '',
20
+ detecting: false,
21
+ detectionError: '',
22
+ editingServiceNames: new Set(),
23
+ };
24
+
25
+ const baseagentLabels = {
26
+ claude: 'Claude Code',
27
+ codex: 'Codex',
28
+ gemini: 'Gemini CLI',
29
+ ecagent: 'EC Agent',
30
+ };
31
+ const baseagentMarks = { claude: 'C', codex: 'X', gemini: 'G', ecagent: 'E' };
32
+ const effortOptions = ['low', 'medium', 'high', 'xhigh', 'max'];
33
+
34
+ const messages = {
35
+ 'zh-CN': {
36
+ 'basicSettings.accessControl': '访问控制',
37
+ 'basicSettings.activeLabel': '默认基座:{value}',
38
+ 'basicSettings.addFallback': '添加备选模型',
39
+ 'basicSettings.addOwner': '添加 Owner',
40
+ 'basicSettings.addService': '添加服务',
41
+ 'basicSettings.agentProgressSummaries': '进度摘要',
42
+ 'basicSettings.agentProgressSummariesHelp': '在 Claude Code 执行过程中显示阶段性进度摘要。',
43
+ 'basicSettings.apiKey': 'API Key',
44
+ 'basicSettings.apiKeyPlaceholder': '输入新值后替换;支持 ${VAR} 或 $ENV:变量名',
45
+ 'basicSettings.showSecret': '显示输入内容',
46
+ 'basicSettings.hideSecret': '隐藏输入内容',
47
+ 'basicSettings.revealSecretFailed': '读取 API Key 失败',
48
+ 'basicSettings.appliesNewSessions': '影响新会话',
49
+ 'basicSettings.approvalsReviewer': '审批审查者',
50
+ 'basicSettings.approvalsReviewerHelp': '决定 Codex 审批请求由哪种审查流程处理。',
51
+ 'basicSettings.aunSdkLog': 'AUN SDK 调试日志',
52
+ 'basicSettings.aunTrace': 'AUN 协议 Trace',
53
+ 'basicSettings.auxiliaryEffort': '辅助模型推理强度',
54
+ 'basicSettings.auxiliaryHelp': '用于 Session Renew 的语义连续性判断。',
55
+ 'basicSettings.auxiliaryModel': '辅助模型',
56
+ 'basicSettings.baseUrl': 'API 地址',
57
+ 'basicSettings.chatMode': '默认聊天方式',
58
+ 'basicSettings.chatModeHelp': 'interactive 仅响应用户消息;proactive 允许在工具执行期间主动反馈。',
59
+ 'basicSettings.cliPath': 'CLI 路径',
60
+ 'basicSettings.cliPathHelp': 'Gemini CLI 可执行文件的路径;留空时使用系统 PATH 查找。',
61
+ 'basicSettings.closeConfirm': '存在未保存的修改,仍要关闭吗?',
62
+ 'basicSettings.configSnapshots': '配置快照',
63
+ 'basicSettings.configSnapshotsHelp': '保存运行时配置快照,便于排查问题;快照可能包含敏感上下文。',
64
+ 'basicSettings.connection': '连接与凭证',
65
+ 'basicSettings.controlIdentity': '控制身份',
66
+ 'basicSettings.daemonAid': 'Daemon AID',
67
+ 'basicSettings.daemonOwners': 'Daemon Owner',
68
+ 'basicSettings.daemonScope': 'daemon.json',
69
+ 'basicSettings.defaultAidDomain': '默认 AID 域名',
70
+ 'basicSettings.defaultAidDomainHelp': '生成控制 AID 或 benchmark AID 时使用,不改变已有 AID。',
71
+ 'basicSettings.defaultBaseagent': '设为默认基座',
72
+ 'basicSettings.defaultEncrypt': '消息默认加密',
73
+ 'basicSettings.defaultFirst': '默认模型优先',
74
+ 'basicSettings.defaultProjectHelp': '新建 Agent 时使用其父目录作为项目根目录,再按 AID 生成项目目录。',
75
+ 'basicSettings.defaultProjectPath': '默认项目目录',
76
+ 'basicSettings.defaultsScope': 'agents/defaults.json',
77
+ 'basicSettings.detected': '已发现',
78
+ 'basicSettings.detectionResult': '检测结果',
79
+ 'basicSettings.detecting': '检测中…',
80
+ 'basicSettings.detectionFailed': '检测失败:{value}',
81
+ 'basicSettings.eckSnapshots': 'ECK 调试快照',
82
+ 'basicSettings.eckSnapshotsHelp': '可能包含敏感上下文;还需 RESPONSE_SNAPSHOT=1 才会实际写入。',
83
+ 'basicSettings.effort': '推理强度',
84
+ 'basicSettings.enableRequestUserInput': '允许请求用户输入',
85
+ 'basicSettings.enableRequestUserInputHelp': '允许 Codex 在需要补充信息时向用户发起输入请求。',
86
+ 'basicSettings.excludeDynamicSections': '排除动态区段',
87
+ 'basicSettings.excludeDynamicSectionsHelp': '从 Claude 请求上下文中排除动态区段,减少不稳定或不需要的内容。',
88
+ 'basicSettings.expandCollapse': '展开或折叠 Base Agent',
89
+ 'basicSettings.fallbackHelp': '按顺序尝试,最多三个;每个备选模型可单独设置推理强度。',
90
+ 'basicSettings.fallbackLimit': '最多只能配置三个备选模型。',
91
+ 'basicSettings.fallbackModels': '备选模型',
92
+ 'basicSettings.fullaccessEnabled': '允许 Fullaccess',
93
+ 'basicSettings.fullaccessHelp': '允许可信 Daemon Owner 使用 /fa 或 /fullaccess,并启用 fullaccess Trigger。此权限可直接访问宿主环境。',
94
+ 'basicSettings.geminiLocation': 'Vertex 区域',
95
+ 'basicSettings.geminiLocationHelp': '使用 Vertex AI 时的 Google Cloud 区域,例如 us-central1。',
96
+ 'basicSettings.geminiMode': '运行模式',
97
+ 'basicSettings.geminiModeHelp': '选择 Gemini CLI 或 SDK 作为调用方式。',
98
+ 'basicSettings.geminiProject': 'Vertex 项目',
99
+ 'basicSettings.geminiProjectHelp': '使用 Vertex AI 时的 Google Cloud 项目 ID。',
100
+ 'basicSettings.headers': '请求头',
101
+ 'basicSettings.headersHelp': '每行一个 key = value;值支持环境变量展开。',
102
+ 'basicSettings.identityIntro': '查看控制身份并设置远程管理与 AUN 安全策略。',
103
+ 'basicSettings.idleEnabled': '空闲监控',
104
+ 'basicSettings.idleEnabledHelp': '监控空闲或无响应任务,并按空闲监控策略执行重试和超时处理。',
105
+ 'basicSettings.idleTimeout': '空闲超时秒数',
106
+ 'basicSettings.keyValuePlaceholder': '每行一个 key = value',
107
+ 'basicSettings.loadFailed': '读取基础设置失败',
108
+ 'basicSettings.loading': '正在读取设置…',
109
+ 'basicSettings.localBaseagents': '本机 Base Agent',
110
+ 'basicSettings.localServices': '本机服务',
111
+ 'basicSettings.logLevel': '日志级别',
112
+ 'basicSettings.logTypes': '日志监听类型',
113
+ 'basicSettings.logTypesHelp': '选择日志监听命令默认关注的日志类型。',
114
+ 'basicSettings.noLogTypes': '暂无可用日志类型',
115
+ 'basicSettings.logTypeFiles': '{value} 个文件',
116
+ 'basicSettings.logsDiagnostics': '日志与诊断',
117
+ 'basicSettings.maxExecutionTime': '单次任务最长秒数',
118
+ 'basicSettings.maxRetries': '最大重试次数',
119
+ 'basicSettings.maxRetriesHelp': '请求失败时最多自动重试的次数。',
120
+ 'basicSettings.maxRetryDelayMs': '重试最大延迟(毫秒)',
121
+ 'basicSettings.maxRetryDelayMsHelp': '自动重试之间允许等待的最大延迟。',
122
+ 'basicSettings.mentionMode': '@ 处理模式',
123
+ 'basicSettings.mentionModeHelp': '控制群聊中是否必须通过 @ 提及当前 Agent 才处理消息。',
124
+ 'basicSettings.menuTokenRequired': '要求 Menu Token',
125
+ 'basicSettings.menuTokenRequiredHelp': '要求远程菜单请求携带有效 Menu Token,避免未授权调用。',
126
+ 'basicSettings.minEvolVersion': '最低客户端版本',
127
+ 'basicSettings.minEvolVersionHelp': '拒绝低于此版本的 EvolCore 客户端连接;留空表示不限制。',
128
+ 'basicSettings.model': '模型',
129
+ 'basicSettings.modelChain': '模型链',
130
+ 'basicSettings.modelsIntro': '只展示本机检测到或已有配置的 Base Agent。每个基座独立配置模型、备用链、辅助模型和连接参数。',
131
+ 'basicSettings.noAgents': '没有检测到或配置过 Base Agent。',
132
+ 'basicSettings.noChanges': '没有需要保存的改动',
133
+ 'basicSettings.noServices': '尚未配置本机服务。',
134
+ 'basicSettings.notDetected': '未发现',
135
+ 'basicSettings.notSet': '未设置',
136
+ 'basicSettings.ownerHelp': '远程管理必备参数,至少保留一个合法且不重复的 AID。',
137
+ 'basicSettings.pathToClaude': 'Claude Code 命令路径',
138
+ 'basicSettings.pathToClaudeHelp': 'Claude Code 可执行文件的绝对路径;留空时使用系统 PATH 查找。',
139
+ 'basicSettings.preToolCheck': '首个工具发送检查',
140
+ 'basicSettings.preToolCheckHelp': '在首次工具调用前先发送一次响应检查,避免工具执行期间无反馈。',
141
+ 'basicSettings.proactiveDetails': '主动响应细节',
142
+ 'basicSettings.proactiveOnly': '仅 proactive 生效',
143
+ 'basicSettings.projectDirectory': '项目目录',
144
+ 'basicSettings.projectsIntro': '设置新 Agent 的项目目录,以及 daemon 管理的本机服务实例。',
145
+ 'basicSettings.queryParams': '查询参数',
146
+ 'basicSettings.queryParamsHelp': '每行一个 key = value,将附加到 Base Agent 请求 URL。',
147
+ 'basicSettings.readonly': '只读',
148
+ 'basicSettings.redetect': '重新检测',
149
+ 'basicSettings.reload': '重新读取',
150
+ 'basicSettings.reloadConfirm': '重新读取会放弃未保存的修改,是否继续?',
151
+ 'basicSettings.remove': '删除',
152
+ 'basicSettings.removeService': '删除服务',
153
+ 'basicSettings.renewAfter': '等待小时数',
154
+ 'basicSettings.renewAfterHelp': '会话空闲超过此时长后触发续接判定。',
155
+ 'basicSettings.renewEnabled': '启用续接判定',
156
+ 'basicSettings.renewEnabledHelp': '启用会话续接分类,用于决定继续当前会话还是创建新会话。',
157
+ 'basicSettings.renewFallback': '判定失败时',
158
+ 'basicSettings.renewFallbackHelp': '续接分类无法完成时采用的后备动作。',
159
+ 'basicSettings.responseIntro': '设置消息响应方式与会话超时后的续接策略。',
160
+ 'basicSettings.responseMode': '响应模式',
161
+ 'basicSettings.responseModeHelp': '选择消息响应所使用的会话模式。',
162
+ 'basicSettings.responseModeSection': '响应模式',
163
+ 'basicSettings.restartNotice': '设置已保存,需要重启 EvolCore 后生效。',
164
+ 'basicSettings.restartPaths': '需要重启的参数:{value}',
165
+ 'basicSettings.apiRetryTimeout': 'API 重试超时秒数',
166
+ 'basicSettings.runtimeIntro': '设置运行保护、日志级别与诊断开关。',
167
+ 'basicSettings.runtimeProtection': '运行保护',
168
+ 'basicSettings.saveFailed': '保存基础设置失败',
169
+ 'basicSettings.saved': '设置已保存',
170
+ 'basicSettings.secretRedacted': '敏感值不回显',
171
+ 'basicSettings.serviceEnabled': '启用本机服务',
172
+ 'basicSettings.serviceName': '服务名称',
173
+ 'basicSettings.editServiceName': '编辑服务名称',
174
+ 'basicSettings.unnamedService': '未命名服务',
175
+ 'basicSettings.servicePort': '期望监听端口',
176
+ 'basicSettings.serviceProxyEnabled': '启用 AUN 代理',
177
+ 'basicSettings.serviceProxyEnabledHelp': '通过 AUN 代理转发此服务的请求;关闭后不会建立代理回连。',
178
+ 'basicSettings.serviceSource': '回连来源',
179
+ 'basicSettings.serviceSourceHelp': '自动发现同名的本机服务实例;ecweb 会查找本机 EC Web。',
180
+ 'basicSettings.serviceSourceStatic': '静态地址',
181
+ 'basicSettings.serviceEndpoint': '回连地址',
182
+ 'basicSettings.serviceEndpointHelp': '仅在回连来源为 static 时使用;必须指向本机 localhost 或 127.0.0.0/8。',
183
+ 'basicSettings.serviceType': '服务类型',
184
+ 'basicSettings.serviceMetadata': 'metadata',
185
+ 'basicSettings.serviceMetadataHelp': '非敏感服务描述,每行一个 key = value;敏感键不会被允许。',
186
+ 'basicSettings.services': '服务列表',
187
+ 'basicSettings.sessionRenew': '会话续接',
188
+ 'basicSettings.showActivities': '活动显示',
189
+ 'basicSettings.showActivitiesHelp': '控制工具执行等活动信息在消息中的显示级别。',
190
+ 'basicSettings.specific': '专属参数',
191
+ 'basicSettings.timeoutMs': '请求超时(毫秒)',
192
+ 'basicSettings.timeoutMsHelp': '单次 EC Agent 请求允许持续的最长时间。',
193
+ 'basicSettings.timeoutPolicy': '超时后判定',
194
+ 'basicSettings.tabs.identity': '身份与安全',
195
+ 'basicSettings.tabs.models': '模型与推理',
196
+ 'basicSettings.tabs.projects': '项目与服务',
197
+ 'basicSettings.tabs.response': '响应与会话',
198
+ 'basicSettings.tabs.runtime': '运行与诊断',
199
+ 'basicSettings.toolReminder': '工具使用提醒',
200
+ 'basicSettings.toolReminderHelp': '在工具执行期间发送工具使用提醒,帮助用户了解当前进度。',
201
+ 'basicSettings.tunnelTargets': '内网穿透目标',
202
+ 'basicSettings.unsaved': '有未保存的修改',
203
+ 'basicSettings.upmsg': 'Daemon 上线通知',
204
+ 'basicSettings.useSettingSources': '使用 Claude 设置来源',
205
+ 'basicSettings.useSettingSourcesHelp': '让 Claude Code 读取其配置文件中的设置来源。',
206
+ 'basicSettings.useVertex': '使用 Vertex AI',
207
+ 'basicSettings.useVertexHelp': '将 Gemini 请求发送到 Google Cloud Vertex AI,而不是默认 Gemini API。',
208
+ 'basicSettings.valuePlaceholder': '请输入值',
209
+ 'basicSettings.visibility': '可见性',
210
+ 'basicSettings.visibilityHelp': 'public 公开访问;private 私有;aun-auth 要求 AUN 身份认证。',
211
+ 'basicSettings.configuredUndetected': '已有配置,但当前未检测到本机运行环境',
212
+ 'basicSettings.validationFailed': '请先修正格式错误:{value}',
213
+ 'basicSettings.ownerRequired': '至少需要一个 Daemon Owner。',
214
+ 'basicSettings.activeRequired': '请选择一个本机可用的默认 Base Agent。',
215
+ 'basicSettings.invalidAid': '请输入合法的多级 AID,例如 owner.agentid.pub。',
216
+ 'basicSettings.duplicateValue': '不能包含重复值。',
217
+ 'basicSettings.invalidUrl': '请输入 http 或 https URL。',
218
+ 'basicSettings.invalidDomain': '请输入合法的多级域名,例如 agentid.cn。',
219
+ 'basicSettings.invalidSemver': '请输入 X.Y.Z 格式的稳定版本号。',
220
+ 'basicSettings.invalidPath': '请输入绝对目录路径。',
221
+ 'basicSettings.invalidNumber': '请输入有效范围内的数字。',
222
+ 'basicSettings.invalidServiceName': '仅允许小写字母、数字、下划线和连字符。',
223
+ 'basicSettings.duplicateService': '服务名称不能重复。',
224
+ 'basicSettings.auxiliaryDependency': '设置辅助模型推理强度前必须先设置辅助模型。',
225
+ 'basicSettings.modelRequired': '请选择模型。',
226
+ 'basicSettings.yes': '是(true)',
227
+ 'basicSettings.no': '否(false)',
228
+ 'basicSettings.autoDiscover': '自动发现',
229
+ 'basicSettings.serviceTypeHttp': 'HTTP',
230
+ 'basicSettings.serviceTypeWebsocket': 'WebSocket',
231
+ 'basicSettings.serviceTypeSsh': 'SSH',
232
+ 'basicSettings.inheritTimeout': '继承 timeout',
233
+ },
234
+ 'en-US': {
235
+ 'basicSettings.accessControl': 'Access control',
236
+ 'basicSettings.activeLabel': 'Default Base Agent: {value}',
237
+ 'basicSettings.addFallback': 'Add fallback model',
238
+ 'basicSettings.addOwner': 'Add Owner',
239
+ 'basicSettings.addService': 'Add service',
240
+ 'basicSettings.agentProgressSummaries': 'Progress summaries',
241
+ 'basicSettings.agentProgressSummariesHelp': 'Show periodic progress summaries while Claude Code is running.',
242
+ 'basicSettings.apiKey': 'API Key',
243
+ 'basicSettings.apiKeyPlaceholder': 'Enter a replacement; ${VAR} and $ENV:NAME are supported',
244
+ 'basicSettings.showSecret': 'Show input',
245
+ 'basicSettings.hideSecret': 'Hide input',
246
+ 'basicSettings.revealSecretFailed': 'Could not read API Key',
247
+ 'basicSettings.appliesNewSessions': 'Applies to new sessions',
248
+ 'basicSettings.approvalsReviewer': 'Approval reviewer',
249
+ 'basicSettings.approvalsReviewerHelp': 'Selects the review flow used for Codex approval requests.',
250
+ 'basicSettings.aunSdkLog': 'AUN SDK debug log',
251
+ 'basicSettings.aunTrace': 'AUN protocol trace',
252
+ 'basicSettings.auxiliaryEffort': 'Auxiliary model effort',
253
+ 'basicSettings.auxiliaryHelp': 'Used by Session Renew to classify semantic continuity.',
254
+ 'basicSettings.auxiliaryModel': 'Auxiliary model',
255
+ 'basicSettings.baseUrl': 'API URL',
256
+ 'basicSettings.chatMode': 'Default chat mode',
257
+ 'basicSettings.chatModeHelp': 'interactive responds to user messages; proactive may report while tools run.',
258
+ 'basicSettings.cliPath': 'CLI path',
259
+ 'basicSettings.cliPathHelp': 'Path to the Gemini CLI executable; leave empty to resolve it from PATH.',
260
+ 'basicSettings.closeConfirm': 'There are unsaved changes. Close anyway?',
261
+ 'basicSettings.configSnapshots': 'Configuration snapshots',
262
+ 'basicSettings.configSnapshotsHelp': 'Save runtime configuration snapshots for troubleshooting; snapshots may contain sensitive context.',
263
+ 'basicSettings.connection': 'Connection & credentials',
264
+ 'basicSettings.controlIdentity': 'Control identity',
265
+ 'basicSettings.daemonAid': 'Daemon AID',
266
+ 'basicSettings.daemonOwners': 'Daemon Owner',
267
+ 'basicSettings.daemonScope': 'daemon.json',
268
+ 'basicSettings.defaultAidDomain': 'Default AID domain',
269
+ 'basicSettings.defaultAidDomainHelp': 'Used for generated control and benchmark AIDs; existing AIDs are unchanged.',
270
+ 'basicSettings.defaultBaseagent': 'Set as default',
271
+ 'basicSettings.defaultEncrypt': 'Encrypt messages by default',
272
+ 'basicSettings.defaultFirst': 'Primary model first',
273
+ 'basicSettings.defaultProjectHelp': 'New Agents use its parent as the project root, then derive a directory from the AID.',
274
+ 'basicSettings.defaultProjectPath': 'Default project directory',
275
+ 'basicSettings.defaultsScope': 'agents/defaults.json',
276
+ 'basicSettings.detected': 'Found',
277
+ 'basicSettings.detectionResult': 'Detection result',
278
+ 'basicSettings.detecting': 'Detecting…',
279
+ 'basicSettings.detectionFailed': 'Detection failed: {value}',
280
+ 'basicSettings.eckSnapshots': 'ECK debug snapshots',
281
+ 'basicSettings.eckSnapshotsHelp': 'May contain sensitive context; RESPONSE_SNAPSHOT=1 is also required.',
282
+ 'basicSettings.effort': 'Reasoning effort',
283
+ 'basicSettings.enableRequestUserInput': 'Allow user input requests',
284
+ 'basicSettings.enableRequestUserInputHelp': 'Allow Codex to request additional input from the user when needed.',
285
+ 'basicSettings.excludeDynamicSections': 'Exclude dynamic sections',
286
+ 'basicSettings.excludeDynamicSectionsHelp': 'Exclude dynamic sections from Claude request context to reduce unstable or unnecessary content.',
287
+ 'basicSettings.expandCollapse': 'Expand or collapse Base Agent',
288
+ 'basicSettings.fallbackHelp': 'Tried in order, up to three; each may use a separate effort.',
289
+ 'basicSettings.fallbackLimit': 'At most three fallback models can be configured.',
290
+ 'basicSettings.fallbackModels': 'Fallback models',
291
+ 'basicSettings.fullaccessEnabled': 'Allow Fullaccess',
292
+ 'basicSettings.fullaccessHelp': 'Allows trusted Daemon Owners to use /fa or /fullaccess and enables fullaccess Triggers. This grants host-level access.',
293
+ 'basicSettings.geminiLocation': 'Vertex location',
294
+ 'basicSettings.geminiLocationHelp': 'Google Cloud region used with Vertex AI, for example us-central1.',
295
+ 'basicSettings.geminiMode': 'Run mode',
296
+ 'basicSettings.geminiModeHelp': 'Choose Gemini CLI or SDK as the invocation method.',
297
+ 'basicSettings.geminiProject': 'Vertex project',
298
+ 'basicSettings.geminiProjectHelp': 'Google Cloud project ID used with Vertex AI.',
299
+ 'basicSettings.headers': 'Request headers',
300
+ 'basicSettings.headersHelp': 'One key = value per line; values support environment expansion.',
301
+ 'basicSettings.identityIntro': 'Inspect the control identity and configure remote administration and AUN security.',
302
+ 'basicSettings.idleEnabled': 'Idle monitor',
303
+ 'basicSettings.idleEnabledHelp': 'Monitor idle or unresponsive tasks and apply the configured retry and timeout policy.',
304
+ 'basicSettings.idleTimeout': 'Idle timeout (seconds)',
305
+ 'basicSettings.keyValuePlaceholder': 'One key = value per line',
306
+ 'basicSettings.loadFailed': 'Could not load Basic Settings',
307
+ 'basicSettings.loading': 'Loading settings…',
308
+ 'basicSettings.localBaseagents': 'Local Base Agents',
309
+ 'basicSettings.localServices': 'Local services',
310
+ 'basicSettings.logLevel': 'Log level',
311
+ 'basicSettings.logTypes': 'Log watch types',
312
+ 'basicSettings.logTypesHelp': 'Default log types selected by the log watch command.',
313
+ 'basicSettings.noLogTypes': 'No log types available',
314
+ 'basicSettings.logTypeFiles': '{value} files',
315
+ 'basicSettings.logsDiagnostics': 'Logs & diagnostics',
316
+ 'basicSettings.maxExecutionTime': 'Maximum time per task (seconds)',
317
+ 'basicSettings.maxRetries': 'Maximum retries',
318
+ 'basicSettings.maxRetriesHelp': 'Maximum number of automatic retries after a request fails.',
319
+ 'basicSettings.maxRetryDelayMs': 'Maximum retry delay (ms)',
320
+ 'basicSettings.maxRetryDelayMsHelp': 'Maximum delay allowed between automatic retries.',
321
+ 'basicSettings.mentionMode': '@ mention mode',
322
+ 'basicSettings.mentionModeHelp': 'Controls whether group messages must mention this Agent with @ before processing.',
323
+ 'basicSettings.menuTokenRequired': 'Require Menu Token',
324
+ 'basicSettings.menuTokenRequiredHelp': 'Require a valid Menu Token on remote menu requests to prevent unauthorized calls.',
325
+ 'basicSettings.minEvolVersion': 'Minimum client version',
326
+ 'basicSettings.minEvolVersionHelp': 'Reject EvolCore clients below this version; leave empty for no minimum.',
327
+ 'basicSettings.model': 'Model',
328
+ 'basicSettings.modelChain': 'Model chain',
329
+ 'basicSettings.modelsIntro': 'Shows locally detected or already configured Base Agents. Each Base Agent owns its models, fallbacks, auxiliary model, and connection.',
330
+ 'basicSettings.noAgents': 'No Base Agent has been detected or configured.',
331
+ 'basicSettings.noChanges': 'No changes to save',
332
+ 'basicSettings.noServices': 'No local services configured.',
333
+ 'basicSettings.notDetected': 'Not found',
334
+ 'basicSettings.notSet': 'Not set',
335
+ 'basicSettings.ownerHelp': 'Required for remote administration; keep at least one valid, unique AID.',
336
+ 'basicSettings.pathToClaude': 'Claude Code executable',
337
+ 'basicSettings.pathToClaudeHelp': 'Absolute path to the Claude Code executable; leave empty to resolve it from PATH.',
338
+ 'basicSettings.preToolCheck': 'First-tool response check',
339
+ 'basicSettings.preToolCheckHelp': 'Send a response check before the first tool call so tool execution does not begin without feedback.',
340
+ 'basicSettings.proactiveDetails': 'Proactive response details',
341
+ 'basicSettings.proactiveOnly': 'Only for proactive mode',
342
+ 'basicSettings.projectDirectory': 'Project directory',
343
+ 'basicSettings.projectsIntro': 'Configure the new-Agent project directory and daemon-managed local service instances.',
344
+ 'basicSettings.queryParams': 'Query parameters',
345
+ 'basicSettings.queryParamsHelp': 'One key = value per line, appended to Base Agent request URLs.',
346
+ 'basicSettings.readonly': 'Read only',
347
+ 'basicSettings.redetect': 'Detect again',
348
+ 'basicSettings.reload': 'Reload',
349
+ 'basicSettings.reloadConfirm': 'Reloading discards unsaved changes. Continue?',
350
+ 'basicSettings.remove': 'Remove',
351
+ 'basicSettings.removeService': 'Remove service',
352
+ 'basicSettings.renewAfter': 'Wait time (hours)',
353
+ 'basicSettings.renewAfterHelp': 'Trigger renewal classification after the session has been idle for this long.',
354
+ 'basicSettings.renewEnabled': 'Enable renewal classification',
355
+ 'basicSettings.renewEnabledHelp': 'Classify session renewal to decide whether to continue or start a new session.',
356
+ 'basicSettings.renewFallback': 'If classification fails',
357
+ 'basicSettings.renewFallbackHelp': 'Fallback action when renewal classification cannot be completed.',
358
+ 'basicSettings.responseIntro': 'Configure message responses and session renewal after inactivity.',
359
+ 'basicSettings.responseMode': 'Response mode',
360
+ 'basicSettings.responseModeHelp': 'Choose the session mode used for message responses.',
361
+ 'basicSettings.responseModeSection': 'Response mode',
362
+ 'basicSettings.restartNotice': 'Settings saved. Restart EvolCore to apply them.',
363
+ 'basicSettings.restartPaths': 'Restart-required fields: {value}',
364
+ 'basicSettings.apiRetryTimeout': 'API retry timeout (seconds)',
365
+ 'basicSettings.runtimeIntro': 'Configure runtime protection, log levels, and diagnostics.',
366
+ 'basicSettings.runtimeProtection': 'Runtime protection',
367
+ 'basicSettings.saveFailed': 'Could not save Basic Settings',
368
+ 'basicSettings.saved': 'Settings saved',
369
+ 'basicSettings.secretRedacted': 'Secrets are never displayed',
370
+ 'basicSettings.serviceEnabled': 'Enable local service',
371
+ 'basicSettings.serviceName': 'Service name',
372
+ 'basicSettings.editServiceName': 'Edit service name',
373
+ 'basicSettings.unnamedService': 'Unnamed service',
374
+ 'basicSettings.servicePort': 'Preferred listen port',
375
+ 'basicSettings.serviceProxyEnabled': 'Enable AUN proxy',
376
+ 'basicSettings.serviceProxyEnabledHelp': 'Forward this service through the AUN proxy; disabling it prevents proxy callbacks.',
377
+ 'basicSettings.serviceSource': 'Callback source',
378
+ 'basicSettings.serviceSourceHelp': 'Automatically discovers the local instance with the same name; ecweb finds local EC Web.',
379
+ 'basicSettings.serviceSourceStatic': 'Static address',
380
+ 'basicSettings.serviceEndpoint': 'Callback address',
381
+ 'basicSettings.serviceEndpointHelp': 'Used only with source=static; it must point to localhost or 127.0.0.0/8.',
382
+ 'basicSettings.serviceType': 'Service type',
383
+ 'basicSettings.serviceMetadata': 'Metadata',
384
+ 'basicSettings.serviceMetadataHelp': 'Non-sensitive service description, one key = value per line; sensitive keys are rejected.',
385
+ 'basicSettings.services': 'Services',
386
+ 'basicSettings.sessionRenew': 'Session renewal',
387
+ 'basicSettings.showActivities': 'Activity display',
388
+ 'basicSettings.showActivitiesHelp': 'Controls how tool execution and other activity details appear in messages.',
389
+ 'basicSettings.specific': 'Agent-specific settings',
390
+ 'basicSettings.timeoutMs': 'Request timeout (ms)',
391
+ 'basicSettings.timeoutMsHelp': 'Maximum duration allowed for a single EC Agent request.',
392
+ 'basicSettings.timeoutPolicy': 'Classify after timeout',
393
+ 'basicSettings.tabs.identity': 'Identity & Security',
394
+ 'basicSettings.tabs.models': 'Models & Reasoning',
395
+ 'basicSettings.tabs.projects': 'Projects & Services',
396
+ 'basicSettings.tabs.response': 'Responses & Sessions',
397
+ 'basicSettings.tabs.runtime': 'Runtime & Diagnostics',
398
+ 'basicSettings.toolReminder': 'Tool-use reminder',
399
+ 'basicSettings.toolReminderHelp': 'Send a reminder while tools run so users can see that work is in progress.',
400
+ 'basicSettings.tunnelTargets': 'Tunnel targets',
401
+ 'basicSettings.unsaved': 'Unsaved changes',
402
+ 'basicSettings.upmsg': 'Daemon online notification',
403
+ 'basicSettings.useSettingSources': 'Use Claude setting sources',
404
+ 'basicSettings.useSettingSourcesHelp': 'Allow Claude Code to read settings from its configured setting sources.',
405
+ 'basicSettings.useVertex': 'Use Vertex AI',
406
+ 'basicSettings.useVertexHelp': 'Send Gemini requests to Google Cloud Vertex AI instead of the default Gemini API.',
407
+ 'basicSettings.valuePlaceholder': 'Enter a value',
408
+ 'basicSettings.visibility': 'Visibility',
409
+ 'basicSettings.visibilityHelp': 'public is open; private is private; aun-auth requires AUN identity authentication.',
410
+ 'basicSettings.configuredUndetected': 'Configured, but the local runtime is not currently detected',
411
+ 'basicSettings.validationFailed': 'Fix this value before saving: {value}',
412
+ 'basicSettings.ownerRequired': 'At least one Daemon Owner is required.',
413
+ 'basicSettings.activeRequired': 'Select a locally available default Base Agent.',
414
+ 'basicSettings.invalidAid': 'Enter a valid multi-level AID, such as owner.agentid.pub.',
415
+ 'basicSettings.duplicateValue': 'Duplicate values are not allowed.',
416
+ 'basicSettings.invalidUrl': 'Enter an HTTP or HTTPS URL.',
417
+ 'basicSettings.invalidDomain': 'Enter a valid multi-level domain, such as agentid.cn.',
418
+ 'basicSettings.invalidSemver': 'Enter a stable X.Y.Z version.',
419
+ 'basicSettings.invalidPath': 'Enter an absolute directory path.',
420
+ 'basicSettings.invalidNumber': 'Enter a number within the valid range.',
421
+ 'basicSettings.invalidServiceName': 'Use lowercase letters, digits, underscores, or hyphens only.',
422
+ 'basicSettings.duplicateService': 'Service names must be unique.',
423
+ 'basicSettings.auxiliaryDependency': 'Set an auxiliary model before its reasoning effort.',
424
+ 'basicSettings.modelRequired': 'Select a model.',
425
+ 'basicSettings.yes': 'Yes (true)',
426
+ 'basicSettings.no': 'No (false)',
427
+ 'basicSettings.autoDiscover': 'Automatic discovery',
428
+ 'basicSettings.serviceTypeHttp': 'HTTP',
429
+ 'basicSettings.serviceTypeWebsocket': 'WebSocket',
430
+ 'basicSettings.serviceTypeSsh': 'SSH',
431
+ 'basicSettings.inheritTimeout': 'Inherit timeout',
432
+ },
433
+ };
434
+
435
+ function currentLanguage() {
436
+ return document.documentElement.lang === 'en-US' ? 'en-US' : 'zh-CN';
437
+ }
438
+
439
+ function t(key, vars = {}) {
440
+ let value = messages[currentLanguage()]?.[key];
441
+ if (value === undefined && typeof window.t === 'function') value = window.t(key);
442
+ if (value === undefined || value === key) value = messages['zh-CN']?.[key] ?? key;
443
+ return String(value).replace(/\{(\w+)\}/g, (_, name) => vars[name] == null ? '' : String(vars[name]));
444
+ }
445
+
446
+ const byId = (id) => document.getElementById(id);
447
+ const htmlEscape = (value) => typeof esc === 'function' ? esc(value) : String(value == null ? '' : value).replace(/[&<>\"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '\"': '&quot;' }[c]));
448
+
449
+ function clone(value) {
450
+ if (value === undefined) return undefined;
451
+ return JSON.parse(JSON.stringify(value));
452
+ }
453
+
454
+ function isObject(value) {
455
+ return !!value && typeof value === 'object' && !Array.isArray(value);
456
+ }
457
+
458
+ function getAt(root, fieldPath) {
459
+ if (!root) return undefined;
460
+ return fieldPath.split('.').reduce((current, segment) => current == null ? undefined : current[segment], root);
461
+ }
462
+
463
+ function hasAt(root, fieldPath) {
464
+ if (!root) return false;
465
+ const segments = fieldPath.split('.');
466
+ let current = root;
467
+ for (const segment of segments) {
468
+ if (!isObject(current) && !Array.isArray(current)) return false;
469
+ if (!Object.prototype.hasOwnProperty.call(current, segment)) return false;
470
+ current = current[segment];
471
+ }
472
+ return true;
473
+ }
474
+
475
+ function setAt(root, fieldPath, value) {
476
+ const segments = fieldPath.split('.');
477
+ let current = root;
478
+ segments.slice(0, -1).forEach((segment) => {
479
+ if (!isObject(current[segment])) current[segment] = {};
480
+ current = current[segment];
481
+ });
482
+ current[segments[segments.length - 1]] = clone(value);
483
+ }
484
+
485
+ function unsetAt(root, fieldPath) {
486
+ const segments = fieldPath.split('.');
487
+ let current = root;
488
+ for (const segment of segments.slice(0, -1)) {
489
+ if (!isObject(current[segment])) return;
490
+ current = current[segment];
491
+ }
492
+ delete current[segments[segments.length - 1]];
493
+ }
494
+
495
+ function getFileValue(file, fieldPath) {
496
+ const changes = state.changes[file];
497
+ if (changes.has(fieldPath)) {
498
+ const item = changes.get(fieldPath);
499
+ return item.op === 'unset' ? undefined : clone(item.value);
500
+ }
501
+ return clone(getAt(state.snapshot?.[file], fieldPath));
502
+ }
503
+
504
+ function hasFileValue(file, fieldPath) {
505
+ const changes = state.changes[file];
506
+ if (changes.has(fieldPath)) return changes.get(fieldPath).op !== 'unset';
507
+ return hasAt(state.snapshot?.[file], fieldPath);
508
+ }
509
+
510
+ function buildFilePreview(file) {
511
+ const result = clone(state.snapshot?.[file] || {});
512
+ for (const [fieldPath, item] of state.changes[file]) {
513
+ if (item.op === 'unset') unsetAt(result, fieldPath);
514
+ else setAt(result, fieldPath, item.value);
515
+ }
516
+ return result;
517
+ }
518
+
519
+ function redactJson(value, path = '') {
520
+ if (Array.isArray(value)) return value.map((item, index) => redactJson(item, `${path}[${index}]`));
521
+ if (!isObject(value)) return value;
522
+ const result = {};
523
+ for (const [key, nested] of Object.entries(value)) {
524
+ const nestedPath = path ? `${path}.${key}` : key;
525
+ if (key === 'apiKey' && nested !== undefined && nested !== null) {
526
+ const envRef = state.snapshot?.secretRefs?.[nestedPath];
527
+ result[key] = envRef && String(nested) === envRef ? envRef : '******';
528
+ } else result[key] = redactJson(nested, nestedPath);
529
+ }
530
+ return result;
531
+ }
532
+
533
+ function recordChange(file, fieldPath, op, value) {
534
+ const originalExists = hasAt(state.snapshot?.[file], fieldPath);
535
+ const originalValue = getAt(state.snapshot?.[file], fieldPath);
536
+ if (op === 'unset' && !originalExists) {
537
+ state.changes[file].delete(fieldPath);
538
+ } else if (op === 'set' && originalExists && JSON.stringify(originalValue) === JSON.stringify(value)) {
539
+ state.changes[file].delete(fieldPath);
540
+ } else {
541
+ state.changes[file].set(fieldPath, op === 'set' ? { op, value: clone(value) } : { op });
542
+ }
543
+ clearNotice();
544
+ updateBasicSettingsStatus();
545
+ }
546
+
547
+ function setError(field, message) {
548
+ const error = field?.querySelector('.basic-settings-error');
549
+ if (error) error.textContent = message || '';
550
+ if (field) field.classList.toggle('has-error', !!message);
551
+ }
552
+
553
+ function parseKeyValueText(raw, label) {
554
+ const result = {};
555
+ const lines = String(raw || '').split(/\r?\n/);
556
+ for (let index = 0; index < lines.length; index += 1) {
557
+ const line = lines[index].trim();
558
+ if (!line) continue;
559
+ const equal = line.indexOf('=');
560
+ if (equal <= 0) throw new Error(currentLanguage() === 'en-US' ? `${label}: line ${index + 1} must use key = value` : `${label}:第 ${index + 1} 行必须是 key = value`);
561
+ const key = line.slice(0, equal).trim();
562
+ const value = line.slice(equal + 1).trim();
563
+ if (!key) throw new Error(currentLanguage() === 'en-US' ? `${label}: line ${index + 1} has an empty key` : `${label}:第 ${index + 1} 行键名不能为空`);
564
+ if (Object.prototype.hasOwnProperty.call(result, key)) throw new Error(currentLanguage() === 'en-US' ? `${label}: duplicate key ${key}` : `${label}:键 ${key} 重复`);
565
+ result[key] = value;
566
+ }
567
+ return result;
568
+ }
569
+
570
+ function mapToText(value) {
571
+ return isObject(value) ? Object.entries(value).map(([key, item]) => `${key} = ${item}`).join('\n') : '';
572
+ }
573
+
574
+ function selectOptions(options, value, labels) {
575
+ return options.map((option) => {
576
+ const text = labels?.[option] || option;
577
+ return `<option value="${htmlEscape(option)}"${String(option) === String(value) ? ' selected' : ''}>${htmlEscape(text)}</option>`;
578
+ }).join('');
579
+ }
580
+
581
+ function modelOptions(agent, value) {
582
+ const models = state.catalogs[agent] || [];
583
+ const ids = models.map((item) => typeof item === 'string' ? item : item.id).filter(Boolean);
584
+ if (value && !ids.includes(value)) ids.unshift(value);
585
+ if (!ids.length) return value
586
+ ? `<option selected value="${htmlEscape(value)}">${htmlEscape(value)}</option>`
587
+ : `<option value="" selected disabled>${htmlEscape(t('basicSettings.loading'))}</option>`;
588
+ return ids.map((id) => `<option value="${htmlEscape(id)}"${id === value ? ' selected' : ''}>${htmlEscape(id)}</option>`).join('');
589
+ }
590
+
591
+ // Match the CLI watcher's shortLogName() so legacy rotated config values
592
+ // select the same logical prefix as the files discovered by the API.
593
+ function shortLogName(value) {
594
+ const baseName = String(value || '').trim().split(/[\\/]/).pop() || '';
595
+ const stem = baseName.replace(/\.(?:log|jsonl|out)(?:\..+)?$/, '');
596
+ return stem
597
+ .replace(/-\d{8}-\d{2}$/, '')
598
+ .replace(/-\d{8}$/, '')
599
+ .replace(/-\d{4}-\d{2}-\d{2}(?:-\d{2})?$/, '');
600
+ }
601
+
602
+ function fieldLabel(label, fieldPath, help, presence) {
603
+ const tip = help ? `<span class="basic-settings-tip" tabindex="0" role="img" aria-label="${htmlEscape(label)} info" data-tooltip="${htmlEscape(help)}">?</span>` : '';
604
+ const toggle = presence ? `<span class="basic-settings-presence${presence.locked ? ' locked' : ''}"><input type="checkbox" data-bs-presence data-file="${presence.file}" data-path="${htmlEscape(fieldPath)}"${presence.configured ? ' checked' : ''}${presence.locked ? ' disabled' : ''} aria-label="${htmlEscape(label)} configured"><span class="basic-settings-presence-track"></span></span>` : '';
605
+ return `<div class="basic-settings-field-label"><span>${htmlEscape(label)}${tip}<span class="basic-settings-path">${htmlEscape(fieldPath)}</span></span>${toggle}</div>`;
606
+ }
607
+
608
+ function scalarField({ file, path, label, value, type = 'text', options, help = '', optional = true, locked = false, sensitive = false, placeholder = '' }) {
609
+ const configured = hasFileValue(file, path);
610
+ const current = value === undefined ? getFileValue(file, path) : value;
611
+ const presence = optional ? { file, configured, locked } : null;
612
+ const unset = optional && !configured;
613
+ let control = '';
614
+ if (type === 'select' || type === 'model' || type === 'boolean') {
615
+ let optionMarkup = '';
616
+ if (type === 'model') optionMarkup = modelOptions(path.split('.')[1], current);
617
+ else if (type === 'boolean') optionMarkup = `<option value="true"${current === true ? ' selected' : ''}>${htmlEscape(t('basicSettings.yes'))}</option><option value="false"${current === false ? ' selected' : ''}>${htmlEscape(t('basicSettings.no'))}</option>`;
618
+ else optionMarkup = selectOptions(options || [], current);
619
+ control = `<select class="basic-settings-control" data-bs-control data-file="${file}" data-path="${htmlEscape(path)}" data-value-type="${type}"${unset || locked ? ' disabled' : ''}>${optionMarkup}</select>`;
620
+ } else {
621
+ const secretRef = sensitive ? state.snapshot?.secretRefs?.[`${file}.${path}`] : '';
622
+ const isSecretReference = !!secretRef && current === secretRef;
623
+ const pendingChange = state.changes[file].has(path);
624
+ const inputType = type === 'number' ? 'number' : (sensitive && !isSecretReference ? 'password' : 'text');
625
+ const valueText = sensitive
626
+ ? (isSecretReference ? secretRef : (pendingChange ? (current == null ? '' : current) : (configured ? '******' : '')))
627
+ : (current == null ? '' : current);
628
+ const sensitivePlaceholder = sensitive && configured
629
+ ? (isSecretReference ? '' : (currentLanguage() === 'en-US' ? '****** Configured. Leave empty to keep it; enter a value to replace it' : '****** 已配置。留空保持当前,输入新值后替换'))
630
+ : placeholder;
631
+ const effectivePlaceholder = sensitivePlaceholder || t('basicSettings.valuePlaceholder');
632
+ const input = `<input class="basic-settings-control" type="${inputType}" value="${htmlEscape(valueText)}" data-bs-control data-file="${file}" data-path="${htmlEscape(path)}" data-value-type="${type}"${sensitive ? ' data-sensitive="true"' : ''}${sensitive && !secretRef && configured && !pendingChange ? ' data-secret-mask="true"' : ''}${unset || locked ? ' disabled' : ''} placeholder="${htmlEscape(effectivePlaceholder)}">`;
633
+ const secretToggle = configured && !isSecretReference && !!state.snapshot?.secrets?.[`${file}.${path}`]
634
+ ? `<button type="button" class="basic-settings-secret-toggle" data-bs-toggle-secret data-file="${file}" data-path="${htmlEscape(path)}" aria-label="${htmlEscape(t('basicSettings.showSecret'))}" title="${htmlEscape(t('basicSettings.showSecret'))}">👁</button>`
635
+ : '';
636
+ control = sensitive
637
+ ? `<div class="basic-settings-secret-control">${input}${secretToggle}</div>`
638
+ : input;
639
+ }
640
+ const unsetView = unset ? `<div class="basic-settings-unset-value">${htmlEscape(t('basicSettings.notSet'))}</div>` : '';
641
+ return `<div class="basic-settings-field${unset ? ' is-unset' : ''}" data-bs-field data-file="${file}" data-path="${htmlEscape(path)}">${fieldLabel(label, path, help, presence)}${unsetView}${control}<div class="basic-settings-error"></div></div>`;
642
+ }
643
+
644
+ function textAreaField({ file, path, label, value, help = '', placeholder = '', rows = 3, full = true, optional = true }) {
645
+ const configured = hasFileValue(file, path);
646
+ const current = value === undefined ? getFileValue(file, path) : value;
647
+ const unset = optional && !configured;
648
+ const draftKey = `${file}:${path}`;
649
+ const text = state.mapDrafts.has(draftKey) ? state.mapDrafts.get(draftKey) : mapToText(current);
650
+ const unsetView = unset ? `<div class="basic-settings-unset-value">${htmlEscape(t('basicSettings.notSet'))}</div>` : '';
651
+ return `<div class="basic-settings-field${full ? ' full' : ''}${unset ? ' is-unset' : ''}" data-bs-field data-file="${file}" data-path="${htmlEscape(path)}">${fieldLabel(label, path, help, optional ? { file, configured, locked: false } : null)}${unsetView}<textarea class="basic-settings-control" rows="${rows}" data-bs-map data-file="${file}" data-path="${htmlEscape(path)}"${unset ? ' disabled' : ''} placeholder="${htmlEscape(placeholder || t('basicSettings.keyValuePlaceholder'))}">${htmlEscape(text)}</textarea><div class="basic-settings-error"></div></div>`;
652
+ }
653
+
654
+ function section(title, hint, body) {
655
+ return `<section class="basic-settings-section"><div class="basic-settings-section-head"><h4>${htmlEscape(title)}</h4><span>${htmlEscape(hint || '')}</span></div>${body}</section>`;
656
+ }
657
+
658
+ function formGrid(body) { return `<div class="basic-settings-form-grid">${body}</div>`; }
659
+
660
+ function fallbackModelsMarkup(agent, config) {
661
+ const rows = Array.isArray(config?.fallbackModels) ? config.fallbackModels : [];
662
+ const body = rows.map((row, index) => `<div class="basic-settings-list-row" data-bs-fallback-row data-agent="${agent}" data-index="${index}"><select class="basic-settings-control" data-bs-fallback-model>${modelOptions(agent, row?.model || '')}</select><select class="basic-settings-control" data-bs-fallback-effort>${selectOptions(effortOptions, row?.effort || 'medium')}</select><button type="button" class="basic-settings-remove" data-bs-remove-fallback aria-label="${htmlEscape(t('basicSettings.remove'))}">×</button></div>`).join('');
663
+ return `<div class="basic-settings-field full" data-bs-field data-path="baseagents.${agent}.fallbackModels" data-bs-fallback-list data-agent="${agent}">${fieldLabel(t('basicSettings.fallbackModels'), `baseagents.${agent}.fallbackModels[]`, t('basicSettings.fallbackHelp'), null)}<div class="basic-settings-list">${body}<button type="button" class="basic-settings-add" data-bs-add-fallback data-agent="${agent}">+ ${htmlEscape(t('basicSettings.addFallback'))}</button></div><div class="basic-settings-error"></div><div class="basic-settings-help">${htmlEscape(t('basicSettings.fallbackHelp'))}</div></div>`;
664
+ }
665
+
666
+ function agentSpecificMarkup(agent, config) {
667
+ const file = 'defaults';
668
+ const p = (name) => `baseagents.${agent}.${name}`;
669
+ if (agent === 'claude') {
670
+ return formGrid(
671
+ scalarField({ file, path: p('pathToClaudeCodeExecutable'), label: t('basicSettings.pathToClaude'), value: config?.pathToClaudeCodeExecutable, placeholder: '/usr/local/bin/claude', help: t('basicSettings.pathToClaudeHelp') }) +
672
+ scalarField({ file, path: p('useSettingSources'), label: t('basicSettings.useSettingSources'), type: 'boolean', value: config?.useSettingSources, help: t('basicSettings.useSettingSourcesHelp') }) +
673
+ scalarField({ file, path: p('agentProgressSummaries'), label: t('basicSettings.agentProgressSummaries'), type: 'boolean', value: config?.agentProgressSummaries, help: t('basicSettings.agentProgressSummariesHelp') }) +
674
+ scalarField({ file, path: p('excludeDynamicSections'), label: t('basicSettings.excludeDynamicSections'), type: 'boolean', value: config?.excludeDynamicSections, help: t('basicSettings.excludeDynamicSectionsHelp') }),
675
+ );
676
+ }
677
+ if (agent === 'codex') {
678
+ return formGrid(
679
+ scalarField({ file, path: p('enableRequestUserInput'), label: t('basicSettings.enableRequestUserInput'), type: 'boolean', value: config?.enableRequestUserInput, help: t('basicSettings.enableRequestUserInputHelp') }) +
680
+ scalarField({ file, path: p('approvalsReviewer'), label: t('basicSettings.approvalsReviewer'), type: 'select', options: ['user', 'auto_review', 'guardian_subagent'], value: config?.approvalsReviewer, help: t('basicSettings.approvalsReviewerHelp') }),
681
+ );
682
+ }
683
+ if (agent === 'gemini') {
684
+ return formGrid(
685
+ scalarField({ file, path: p('cliPath'), label: t('basicSettings.cliPath'), value: config?.cliPath, placeholder: 'gemini', help: t('basicSettings.cliPathHelp') }) +
686
+ scalarField({ file, path: p('mode'), label: t('basicSettings.geminiMode'), type: 'select', options: ['cli', 'sdk'], value: config?.mode, help: t('basicSettings.geminiModeHelp') }) +
687
+ scalarField({ file, path: p('useVertex'), label: t('basicSettings.useVertex'), type: 'boolean', value: config?.useVertex, help: t('basicSettings.useVertexHelp') }) +
688
+ scalarField({ file, path: p('project'), label: t('basicSettings.geminiProject'), value: config?.project, help: t('basicSettings.geminiProjectHelp') }) +
689
+ scalarField({ file, path: p('location'), label: t('basicSettings.geminiLocation'), value: config?.location, help: t('basicSettings.geminiLocationHelp') }),
690
+ );
691
+ }
692
+ return formGrid(
693
+ scalarField({ file, path: p('timeoutMs'), label: t('basicSettings.timeoutMs'), type: 'number', value: config?.timeoutMs, placeholder: '300000', help: t('basicSettings.timeoutMsHelp') }) +
694
+ scalarField({ file, path: p('maxRetries'), label: t('basicSettings.maxRetries'), type: 'number', value: config?.maxRetries, placeholder: '2', help: t('basicSettings.maxRetriesHelp') }) +
695
+ scalarField({ file, path: p('maxRetryDelayMs'), label: t('basicSettings.maxRetryDelayMs'), type: 'number', value: config?.maxRetryDelayMs, placeholder: '8000', help: t('basicSettings.maxRetryDelayMsHelp') }),
696
+ );
697
+ }
698
+
699
+ function agentCard(agent, details, defaults) {
700
+ const config = defaults?.baseagents?.[agent] || {};
701
+ const active = getFileValue('defaults', 'active_baseagent') === agent;
702
+ const open = state.openAgents.has(agent);
703
+ const source = details.detected
704
+ ? (details.detectionSource?.length ? details.detectionSource.join(', ') : t('basicSettings.detected'))
705
+ : t('basicSettings.configuredUndetected');
706
+ const common = (name) => `baseagents.${agent}.${name}`;
707
+ const model = scalarField({ file: 'defaults', path: common('model'), label: t('basicSettings.model'), type: 'model', value: config.model, optional: true });
708
+ const effort = agent === 'gemini' ? '' : scalarField({ file: 'defaults', path: common('effort'), label: t('basicSettings.effort'), type: 'select', options: effortOptions, value: config.effort });
709
+ const auxiliary = scalarField({ file: 'defaults', path: common('auxiliaryModel'), label: t('basicSettings.auxiliaryModel'), type: 'model', value: config.auxiliaryModel, help: t('basicSettings.auxiliaryHelp') });
710
+ const auxiliaryEffort = scalarField({ file: 'defaults', path: common('auxiliaryEffort'), label: t('basicSettings.auxiliaryEffort'), type: 'select', options: effortOptions, value: config.auxiliaryEffort });
711
+ const connection = scalarField({ file: 'defaults', path: common('apiKey'), label: t('basicSettings.apiKey'), value: config.apiKey, sensitive: true, placeholder: t('basicSettings.apiKeyPlaceholder') }) +
712
+ (agent === 'gemini' ? '' : scalarField({ file: 'defaults', path: common('baseUrl'), label: t('basicSettings.baseUrl'), value: config.baseUrl, placeholder: 'https://api.example.com' })) +
713
+ (agent === 'gemini' ? '' : textAreaField({ file: 'defaults', path: common('headers'), label: t('basicSettings.headers'), value: config.headers, placeholder: t('basicSettings.keyValuePlaceholder'), help: t('basicSettings.headersHelp'), full: false })) +
714
+ (['claude', 'codex', 'ecagent'].includes(agent) ? textAreaField({ file: 'defaults', path: common('queryParams'), label: t('basicSettings.queryParams'), value: config.queryParams, placeholder: t('basicSettings.keyValuePlaceholder'), help: t('basicSettings.queryParamsHelp'), full: false }) : '');
715
+ const body = section(t('basicSettings.connection'), t('basicSettings.secretRedacted'), formGrid(connection)) +
716
+ section(t('basicSettings.modelChain'), t('basicSettings.defaultFirst'), formGrid(model + effort + fallbackModelsMarkup(agent, config) + auxiliary + auxiliaryEffort)) +
717
+ section(t('basicSettings.specific'), `${baseagentLabels[agent]} runner`, agentSpecificMarkup(agent, config));
718
+ return `<article class="basic-settings-agent${active ? ' active' : ''}${open ? ' open' : ''}${details.detected ? '' : ' unavailable'}" data-bs-agent="${agent}"><header class="basic-settings-agent-head" data-bs-agent-toggle><span class="basic-settings-agent-mark">${baseagentMarks[agent]}</span><span class="basic-settings-agent-title"><strong>${htmlEscape(baseagentLabels[agent])}</strong><small>${htmlEscape(details.detected ? t('basicSettings.detected') : t('basicSettings.notDetected'))} · ${htmlEscape(source)}</small></span><label class="basic-settings-agent-default"><input type="radio" name="basic-active-baseagent" value="${agent}" data-bs-active-baseagent${active ? ' checked' : ''}${details.detected ? '' : ' disabled'}>${htmlEscape(t('basicSettings.defaultBaseagent'))}</label><button type="button" class="basic-settings-chevron" data-bs-agent-toggle aria-expanded="${open ? 'true' : 'false'}" aria-label="${htmlEscape(t('basicSettings.expandCollapse'))}"></button></header><div class="basic-settings-agent-body">${body}</div></article>`;
719
+ }
720
+
721
+ function renderModels() {
722
+ const defaults = buildFilePreview('defaults');
723
+ const details = (state.snapshot.baseagents || []).filter((item) => item.detected || item.configured);
724
+ const detection = state.detecting
725
+ ? `<span class="basic-settings-detection-state">${htmlEscape(t('basicSettings.detecting'))}</span>`
726
+ : state.detectionError
727
+ ? `<span class="basic-settings-detection-state error">${htmlEscape(t('basicSettings.detectionFailed', { value: state.detectionError }))}</span>`
728
+ : (state.snapshot.baseagents || []).map((item) => `<span class="basic-settings-detection-item${item.detected ? ' available' : ''}">${htmlEscape(baseagentLabels[item.id] || item.id)}</span>`).join('');
729
+ const cards = details.length ? details.map((item) => agentCard(item.id, item, defaults)).join('') : `<div class="basic-settings-empty">${htmlEscape(t('basicSettings.noAgents'))}</div>`;
730
+ return panelIntro(t('basicSettings.tabs.models'), t('basicSettings.modelsIntro')) + section(t('basicSettings.localBaseagents'), t('basicSettings.activeLabel', { value: getFileValue('defaults', 'active_baseagent') || t('basicSettings.notSet') }), `<div class="basic-settings-active-field" data-bs-field data-file="defaults" data-path="active_baseagent"><div class="basic-settings-detection"><strong>${htmlEscape(t('basicSettings.detectionResult'))}</strong>${detection}<button type="button" class="basic-settings-plain-btn" data-bs-redetect${state.detecting ? ' disabled' : ''}>${htmlEscape(t('basicSettings.redetect'))}</button></div><div class="basic-settings-error"></div></div><div class="basic-settings-agent-list">${cards}</div>`);
731
+ }
732
+
733
+ function panelIntro(title, copy) {
734
+ return `<div class="basic-settings-panel-intro"><h3>${htmlEscape(title)}</h3><p>${htmlEscape(copy)}</p></div>`;
735
+ }
736
+
737
+ function renderResponse() {
738
+ const chatMode = getFileValue('defaults', 'config.chatMode') || 'interactive';
739
+ const body = scalarField({ file: 'defaults', path: 'responseMode', label: t('basicSettings.responseMode'), type: 'select', options: ['single-session'], value: getFileValue('defaults', 'responseMode'), help: t('basicSettings.responseModeHelp') }) +
740
+ scalarField({ file: 'defaults', path: 'config.chatMode', label: t('basicSettings.chatMode'), type: 'select', options: ['interactive', 'proactive'], value: chatMode, help: t('basicSettings.chatModeHelp') }) +
741
+ scalarField({ file: 'defaults', path: 'config.mentionMode', label: t('basicSettings.mentionMode'), type: 'select', options: ['disabled', 'mention-only'], value: getFileValue('defaults', 'config.mentionMode'), help: t('basicSettings.mentionModeHelp') }) +
742
+ scalarField({ file: 'defaults', path: 'config.show_activities', label: t('basicSettings.showActivities'), type: 'select', options: ['all', 'text', 'none'], value: getFileValue('defaults', 'config.show_activities'), help: t('basicSettings.showActivitiesHelp') });
743
+ const proactive = chatMode === 'proactive'
744
+ ? section(t('basicSettings.proactiveDetails'), t('basicSettings.proactiveOnly'), formGrid(
745
+ scalarField({ file: 'defaults', path: 'responseModeParams.single-session.pre_tool_1stmsgchk', label: t('basicSettings.preToolCheck'), type: 'boolean', value: getFileValue('defaults', 'responseModeParams.single-session.pre_tool_1stmsgchk'), help: t('basicSettings.preToolCheckHelp') }) +
746
+ scalarField({ file: 'defaults', path: 'responseModeParams.single-session.tool_use_reminder', label: t('basicSettings.toolReminder'), type: 'boolean', value: getFileValue('defaults', 'responseModeParams.single-session.tool_use_reminder'), help: t('basicSettings.toolReminderHelp') }),
747
+ )) : '';
748
+ const renew = formGrid(
749
+ scalarField({ file: 'defaults', path: 'session_renew.enabled', label: t('basicSettings.renewEnabled'), type: 'boolean', value: getFileValue('defaults', 'session_renew.enabled'), help: t('basicSettings.renewEnabledHelp') }) +
750
+ scalarField({ file: 'defaults', path: 'session_renew.after_hours', label: t('basicSettings.renewAfter'), type: 'number', value: getFileValue('defaults', 'session_renew.after_hours'), placeholder: '24', help: t('basicSettings.renewAfterHelp') }) +
751
+ scalarField({ file: 'defaults', path: 'session_renew.fallback_action', label: t('basicSettings.renewFallback'), type: 'select', options: ['continue', 'new'], value: getFileValue('defaults', 'session_renew.fallback_action'), help: t('basicSettings.renewFallbackHelp') }),
752
+ );
753
+ return panelIntro(t('basicSettings.tabs.response'), t('basicSettings.responseIntro')) + section(t('basicSettings.responseModeSection'), t('basicSettings.appliesNewSessions'), formGrid(body)) + proactive + section(t('basicSettings.sessionRenew'), t('basicSettings.timeoutPolicy'), renew);
754
+ }
755
+
756
+ function serviceMarkup(service, index) {
757
+ const proxy = service?.proxy || {};
758
+ const tip = (label, help) => `${htmlEscape(label)}<span class="basic-settings-tip" tabindex="0" role="img" aria-label="${htmlEscape(label)} info" data-tooltip="${htmlEscape(help)}">?</span>`;
759
+ const editingName = state.editingServiceNames.has(String(index));
760
+ const proxyEnabled = proxy.enabled === true;
761
+ const source = proxy.source === 'static' || (proxy.source === undefined && proxy.endpoint) ? 'static' : 'instance';
762
+ const serviceType = ['http', 'websocket', 'ssh'].includes(proxy.serviceType) ? proxy.serviceType : 'http';
763
+ const metadataKey = String(index);
764
+ const metadataConfigured = Object.prototype.hasOwnProperty.call(proxy, 'metadata');
765
+ const metadata = state.serviceMetadataDrafts.has(metadataKey)
766
+ ? state.serviceMetadataDrafts.get(metadataKey)
767
+ : mapToText(proxy.metadata);
768
+ const metadataPresence = `<span class="basic-settings-presence"><input type="checkbox" data-bs-service-metadata-presence data-bs-service-index="${index}"${metadataConfigured ? ' checked' : ''} aria-label="${htmlEscape(t('basicSettings.serviceMetadata'))} configured"><span class="basic-settings-presence-track"></span></span>`;
769
+ const metadataUnsetView = metadataConfigured ? '' : `<div class="basic-settings-unset-value">${htmlEscape(t('basicSettings.notSet'))}</div>`;
770
+ return `<div class="basic-settings-service" data-bs-service data-index="${index}">
771
+ <div class="basic-settings-service-title-row">
772
+ <div class="basic-settings-service-title">${editingName
773
+ ? `<input class="basic-settings-control basic-settings-service-name-input" data-bs-service-name-input data-bs-service-field="name" value="${htmlEscape(service?.name || '')}" placeholder="${htmlEscape(t('basicSettings.unnamedService'))}" aria-label="${htmlEscape(t('basicSettings.serviceName'))}">`
774
+ : `<strong>${htmlEscape(service?.name || t('basicSettings.unnamedService'))}</strong>`}
775
+ </div>
776
+ <button type="button" class="basic-settings-service-edit" data-bs-edit-service-name data-bs-service-index="${index}" title="${htmlEscape(t('basicSettings.editServiceName'))}" aria-label="${htmlEscape(t('basicSettings.editServiceName'))}">${editingName ? '✓' : '✎'}</button>
777
+ </div>
778
+ <div class="basic-settings-service-grid">
779
+ <label class="basic-settings-service-field"><span>${htmlEscape(t('basicSettings.serviceEnabled'))}</span><select class="basic-settings-control" data-bs-service-field="enabled"><option value="true"${service?.enabled !== false ? ' selected' : ''}>${htmlEscape(t('basicSettings.yes'))}</option><option value="false"${service?.enabled === false ? ' selected' : ''}>${htmlEscape(t('basicSettings.no'))}</option></select></label>
780
+ <label class="basic-settings-service-field"><span>${htmlEscape(t('basicSettings.servicePort'))}</span><input class="basic-settings-control" type="number" min="1" max="65535" data-bs-service-field="port" value="${service?.port == null ? '' : htmlEscape(service.port)}" placeholder="42705"></label>
781
+ <label class="basic-settings-service-field"><span>${tip(t('basicSettings.serviceProxyEnabled'), t('basicSettings.serviceProxyEnabledHelp'))}</span><select class="basic-settings-control" data-bs-service-field="proxy.enabled"><option value="true"${proxy.enabled === true ? ' selected' : ''}>${htmlEscape(t('basicSettings.yes'))}</option><option value="false"${proxy.enabled !== true ? ' selected' : ''}>${htmlEscape(t('basicSettings.no'))}</option></select></label>
782
+ ${proxyEnabled ? `<label class="basic-settings-service-field"><span>${tip(t('basicSettings.serviceSource'), t('basicSettings.serviceSourceHelp'))}</span><select class="basic-settings-control" data-bs-service-field="proxy.source"><option value="instance"${source === 'instance' ? ' selected' : ''}>${htmlEscape(t('basicSettings.autoDiscover'))}</option><option value="static"${source === 'static' ? ' selected' : ''}>${htmlEscape(t('basicSettings.serviceSourceStatic'))}</option></select></label>
783
+ <label class="basic-settings-service-field"><span>${htmlEscape(t('basicSettings.serviceType'))}</span><select class="basic-settings-control" data-bs-service-field="proxy.serviceType"><option value="http"${serviceType === 'http' ? ' selected' : ''}>${htmlEscape(t('basicSettings.serviceTypeHttp'))}</option><option value="websocket"${serviceType === 'websocket' ? ' selected' : ''}>${htmlEscape(t('basicSettings.serviceTypeWebsocket'))}</option><option value="ssh"${serviceType === 'ssh' ? ' selected' : ''}>${htmlEscape(t('basicSettings.serviceTypeSsh'))}</option></select></label>
784
+ <label class="basic-settings-service-field"><span>${tip(t('basicSettings.visibility'), t('basicSettings.visibilityHelp'))}</span><select class="basic-settings-control" data-bs-service-field="proxy.visibility"><option value="private"${proxy.visibility === 'private' || !proxy.visibility ? ' selected' : ''}>private</option><option value="public"${proxy.visibility === 'public' ? ' selected' : ''}>public</option><option value="aun-auth"${proxy.visibility === 'aun-auth' ? ' selected' : ''}>aun-auth</option></select></label>
785
+ <label class="basic-settings-service-field full"${source === 'instance' ? ' hidden' : ''}><span>${tip(t('basicSettings.serviceEndpoint'), t('basicSettings.serviceEndpointHelp'))}</span><input class="basic-settings-control" data-bs-service-field="proxy.endpoint" value="${htmlEscape(proxy.endpoint || '')}"${source === 'instance' ? ' disabled' : ''} placeholder="http://127.0.0.1:3000"></label>
786
+ <div class="basic-settings-service-field full${metadataConfigured ? '' : ' is-unset'}" data-bs-service-metadata-field><div class="basic-settings-service-field-label"><span>${tip(t('basicSettings.serviceMetadata'), t('basicSettings.serviceMetadataHelp'))}</span>${metadataPresence}</div>${metadataUnsetView}<textarea class="basic-settings-control" rows="3" data-bs-service-map data-bs-service-field="proxy.metadata" data-bs-service-index="${index}"${metadataConfigured ? '' : ' disabled'} placeholder="${htmlEscape(t('basicSettings.keyValuePlaceholder'))}">${htmlEscape(metadata)}</textarea><div class="basic-settings-error"></div></div>` : ''}
787
+ </div>
788
+ <div class="basic-settings-service-actions"><button type="button" class="basic-settings-remove" data-bs-remove-service>${htmlEscape(t('basicSettings.removeService'))}</button></div>
789
+ </div>`;
790
+ }
791
+
792
+ function renderProjects() {
793
+ const daemon = buildFilePreview('daemon');
794
+ const services = Array.isArray(daemon.services) ? daemon.services : [];
795
+ const serviceBody = services.map(serviceMarkup).join('');
796
+ return panelIntro(t('basicSettings.tabs.projects'), t('basicSettings.projectsIntro')) + section(t('basicSettings.projectDirectory'), t('basicSettings.defaultsScope'), formGrid(
797
+ scalarField({ file: 'defaults', path: 'projects.defaultPath', label: t('basicSettings.defaultProjectPath'), value: getFileValue('defaults', 'projects.defaultPath'), placeholder: '/path/to/projects', help: t('basicSettings.defaultProjectHelp') }),
798
+ )) + section(t('basicSettings.localServices'), t('basicSettings.daemonScope'), formGrid(
799
+ `<div class="basic-settings-field full" data-bs-field data-file="daemon" data-path="services"><div class="basic-settings-field-label"><span>${htmlEscape(t('basicSettings.services'))}<span class="basic-settings-path">services[]</span></span></div><div class="basic-settings-service-list" id="basic-settings-services">${serviceBody || `<div class="basic-settings-empty">${htmlEscape(t('basicSettings.noServices'))}</div>`}<button type="button" class="basic-settings-add" data-bs-add-service>+ ${htmlEscape(t('basicSettings.addService'))}</button></div><div class="basic-settings-error"></div></div>`,
800
+ ));
801
+ }
802
+
803
+ function ownersMarkup(daemon) {
804
+ const owners = Array.isArray(daemon.owners) ? daemon.owners : [];
805
+ const rows = owners.map((owner) => `<div class="basic-settings-list-row single" data-bs-owner-row><input class="basic-settings-control" data-bs-owner value="${htmlEscape(owner)}" placeholder="owner.agentid.pub"><button type="button" class="basic-settings-remove" data-bs-remove-owner aria-label="remove owner">×</button></div>`).join('');
806
+ return `<div class="basic-settings-collection-card"><div class="basic-settings-list" id="basic-settings-owners">${rows}<button type="button" class="basic-settings-add" data-bs-add-owner>+ ${htmlEscape(t('basicSettings.addOwner'))}</button></div></div><div class="basic-settings-error"></div><div class="basic-settings-help">${htmlEscape(t('basicSettings.ownerHelp'))}</div>`;
807
+ }
808
+
809
+ function renderIdentity() {
810
+ const daemon = buildFilePreview('daemon');
811
+ const aid = daemon.aid || t('basicSettings.notSet');
812
+ return panelIntro(t('basicSettings.tabs.identity'), t('basicSettings.identityIntro')) + section(t('basicSettings.controlIdentity'), t('basicSettings.readonly'), `<div class="basic-settings-form-grid"><div class="basic-settings-field" data-bs-field data-file="daemon" data-path="aid"><div class="basic-settings-field-label"><span>${htmlEscape(t('basicSettings.daemonAid'))}<span class="basic-settings-path">aid</span></span></div><div class="basic-settings-readonly"><code>${htmlEscape(aid)}</code></div></div>${scalarField({ file: 'daemon', path: 'aun.defaultAidDomain', label: t('basicSettings.defaultAidDomain'), value: getFileValue('daemon', 'aun.defaultAidDomain'), placeholder: 'agentid.cn', help: t('basicSettings.defaultAidDomainHelp') })}</div>`) + section(t('basicSettings.accessControl'), t('basicSettings.daemonScope'), `<div class="basic-settings-form-grid"><div class="basic-settings-field full" data-bs-field data-file="daemon" data-path="owners">${fieldLabel(t('basicSettings.daemonOwners'), 'owners[]', t('basicSettings.ownerHelp'), null)}${ownersMarkup(daemon)}</div>${scalarField({ file: 'daemon', path: 'aun.minEvolVersion', label: t('basicSettings.minEvolVersion'), value: getFileValue('daemon', 'aun.minEvolVersion'), placeholder: '1.6.0', help: t('basicSettings.minEvolVersionHelp') })}${scalarField({ file: 'daemon', path: 'aun.menuTokenRequired', label: t('basicSettings.menuTokenRequired'), type: 'boolean', value: getFileValue('daemon', 'aun.menuTokenRequired'), help: t('basicSettings.menuTokenRequiredHelp') })}${scalarField({ file: 'daemon', path: 'aun.defaultEncrypt', label: t('basicSettings.defaultEncrypt'), type: 'boolean', value: getFileValue('daemon', 'aun.defaultEncrypt') })}${scalarField({ file: 'daemon', path: 'fullaccess.enabled', label: t('basicSettings.fullaccessEnabled'), type: 'boolean', value: daemon.fullaccess?.enabled, help: t('basicSettings.fullaccessHelp') })}</div>`);
813
+ }
814
+
815
+ function renderRuntime() {
816
+ const daemon = buildFilePreview('daemon');
817
+ const logTypes = Array.isArray(state.snapshot?.logTypes) ? state.snapshot.logTypes : [];
818
+ const logTypeCounts = state.snapshot?.logTypeCounts || {};
819
+ const selected = Array.isArray(daemon.watch?.logTypes)
820
+ ? new Set(daemon.watch.logTypes.map(shortLogName))
821
+ : new Set(logTypes);
822
+ const logChecks = logTypes.map((type) => {
823
+ const count = Number(logTypeCounts[type]) || 0;
824
+ return `<label class="basic-settings-check basic-settings-log-type"><input type="checkbox" data-bs-log-type value="${htmlEscape(type)}"${selected.has(type) ? ' checked' : ''}>${htmlEscape(type)}<small>${htmlEscape(t('basicSettings.logTypeFiles', { value: count }))}</small></label>`;
825
+ }).join('');
826
+ const debug = daemon.debug || {};
827
+ return panelIntro(t('basicSettings.tabs.runtime'), t('basicSettings.runtimeIntro')) + section(t('basicSettings.runtimeProtection'), t('basicSettings.daemonScope'), formGrid(
828
+ scalarField({ file: 'daemon', path: 'idleMonitor.enabled', label: t('basicSettings.idleEnabled'), type: 'boolean', value: getFileValue('daemon', 'idleMonitor.enabled'), help: t('basicSettings.idleEnabledHelp') }) +
829
+ scalarField({ file: 'daemon', path: 'idleMonitor.timeout', label: t('basicSettings.idleTimeout'), type: 'number', value: getFileValue('daemon', 'idleMonitor.timeout'), placeholder: '120' }) +
830
+ scalarField({ file: 'daemon', path: 'idleMonitor.apiRetryTimeout', label: t('basicSettings.apiRetryTimeout'), type: 'number', value: getFileValue('daemon', 'idleMonitor.apiRetryTimeout'), placeholder: t('basicSettings.inheritTimeout') }) +
831
+ scalarField({ file: 'daemon', path: 'idleMonitor.maxExecutionTime', label: t('basicSettings.maxExecutionTime'), type: 'number', value: getFileValue('daemon', 'idleMonitor.maxExecutionTime'), placeholder: '0' }),
832
+ )) + section(t('basicSettings.logsDiagnostics'), t('basicSettings.daemonScope'), formGrid(
833
+ scalarField({ file: 'daemon', path: 'debug.logLevel', label: t('basicSettings.logLevel'), type: 'select', options: ['DEBUG', 'INFO', 'WARN', 'ERROR'], value: debug.logLevel }) +
834
+ scalarField({ file: 'daemon', path: 'configSnapshots', label: t('basicSettings.configSnapshots'), type: 'boolean', value: getFileValue('daemon', 'configSnapshots'), help: t('basicSettings.configSnapshotsHelp') }) +
835
+ scalarField({ file: 'daemon', path: 'debug.aunTrace', label: t('basicSettings.aunTrace'), type: 'boolean', value: debug.aunTrace }) +
836
+ scalarField({ file: 'daemon', path: 'debug.aunSdkLog', label: t('basicSettings.aunSdkLog'), type: 'boolean', value: debug.aunSdkLog }) +
837
+ scalarField({ file: 'daemon', path: 'debug.upmsg', label: t('basicSettings.upmsg'), type: 'boolean', value: debug.upmsg }) +
838
+ scalarField({ file: 'daemon', path: 'debug.eckSnapshots', label: t('basicSettings.eckSnapshots'), type: 'boolean', value: debug.eckSnapshots, help: t('basicSettings.eckSnapshotsHelp') }) +
839
+ `<div class="basic-settings-field full"><div class="basic-settings-field-label"><span>${htmlEscape(t('basicSettings.logTypes'))}<span class="basic-settings-path">watch.logTypes[]</span></span></div><div class="basic-settings-collection-card"><div class="basic-settings-checkboxes" id="basic-settings-log-types">${logChecks || `<div class="basic-settings-empty">${htmlEscape(t('basicSettings.noLogTypes'))}</div>`}</div></div><div class="basic-settings-help">${htmlEscape(t('basicSettings.logTypesHelp'))}</div></div>`,
840
+ )) + (Array.isArray(daemon.tunnel?.targets) ? section(t('basicSettings.tunnelTargets'), t('basicSettings.readonly'), `<pre class="basic-settings-readonly-json">${htmlEscape(JSON.stringify(daemon.tunnel.targets, null, 2))}</pre>`) : '');
841
+ }
842
+
843
+ function render() {
844
+ const content = byId('basic-settings-content');
845
+ if (!content) return;
846
+ if (state.loading) { content.innerHTML = `<div class="basic-settings-loading">${htmlEscape(t('basicSettings.loading'))}</div>`; return; }
847
+ if (state.error) { content.innerHTML = `<div class="basic-settings-error-state">${htmlEscape(state.error)}<button type="button" class="basic-settings-plain-btn" data-bs-refresh>${htmlEscape(t('basicSettings.reload'))}</button></div>`; updateBasicSettingsStatus(); return; }
848
+ if (!state.snapshot) { content.innerHTML = `<div class="basic-settings-loading">${htmlEscape(t('basicSettings.loading'))}</div>`; return; }
849
+ const html = state.tab === 'models' ? renderModels() : state.tab === 'response' ? renderResponse() : state.tab === 'projects' ? renderProjects() : state.tab === 'identity' ? renderIdentity() : renderRuntime();
850
+ content.innerHTML = `<div class="basic-settings-panel">${html}</div>`;
851
+ updateBasicSettingsStatus();
852
+ }
853
+
854
+ function updateBasicSettingsStatus() {
855
+ const el = byId('basic-settings-status');
856
+ if (!el) return;
857
+ const dirty = state.changes.daemon.size + state.changes.defaults.size > 0;
858
+ el.textContent = state.error || state.notice || (dirty ? t('basicSettings.unsaved') : (state.restartPaths.length ? t('basicSettings.restartNotice') : t('basicSettings.noChanges')));
859
+ el.className = `basic-settings-status${state.error ? ' error' : state.noticeType === 'error' ? ' error' : dirty ? ' dirty' : state.restartPaths.length || state.noticeType === 'success' ? ' saved' : ''}`;
860
+ const saveButton = byId('basic-settings-save');
861
+ if (saveButton) saveButton.disabled = state.saving || !dirty;
862
+ }
863
+
864
+ function clearNotice() {
865
+ state.notice = '';
866
+ state.noticeType = '';
867
+ }
868
+
869
+ function validationError(file, path, message) {
870
+ return { file, path, message };
871
+ }
872
+
873
+ function isValidAid(value) {
874
+ return /^[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]*[A-Za-z0-9])?){2,}$/.test(value);
875
+ }
876
+
877
+ function isValidDomain(value) {
878
+ return /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)+$/.test(value)
879
+ && !/^(?:\d{1,3}\.){3}\d{1,3}$/.test(value)
880
+ && !value.toLowerCase().endsWith('.localhost');
881
+ }
882
+
883
+ function isHttpUrl(value) {
884
+ try {
885
+ const url = new URL(value);
886
+ return url.protocol === 'http:' || url.protocol === 'https:';
887
+ } catch {
888
+ return false;
889
+ }
890
+ }
891
+
892
+ function isServiceEndpoint(value) {
893
+ try {
894
+ const url = new URL(value);
895
+ if (!['http:', 'https:', 'ws:', 'wss:'].includes(url.protocol)) return false;
896
+ const host = url.hostname.toLowerCase().replace(/^\[|\]$/g, '');
897
+ return host === 'localhost' || /^127(?:\.\d{1,3}){3}$/.test(host);
898
+ } catch {
899
+ return false;
900
+ }
901
+ }
902
+
903
+ function hasSensitiveMetadataKey(key) {
904
+ return /(?:^|[_-])(endpoint|url|uri|token|access[_-]?token|authorization|cookie|secret|password|private[_-]?key|key|cert|certificate)(?:$|[_-])/i.test(key);
905
+ }
906
+
907
+ function tabForValidation(error) {
908
+ if (error.file === 'defaults') {
909
+ if (error.path.startsWith('baseagents.') || error.path === 'active_baseagent') return 'models';
910
+ if (error.path.startsWith('projects.')) return 'projects';
911
+ return 'response';
912
+ }
913
+ if (error.path === 'services') return 'projects';
914
+ if (error.path === 'owners' || error.path.startsWith('aun.')) return 'identity';
915
+ return 'runtime';
916
+ }
917
+
918
+ function findFieldForValidation(error) {
919
+ const selector = `[data-bs-field][data-file="${CSS.escape(error.file)}"][data-path="${CSS.escape(error.path)}"]`;
920
+ return document.querySelector(selector) || document.querySelector(`[data-bs-field][data-path="${CSS.escape(error.path)}"]`);
921
+ }
922
+
923
+ function validateBeforeSave() {
924
+ const errors = [];
925
+ const daemon = buildFilePreview('daemon');
926
+ const defaults = buildFilePreview('defaults');
927
+ const push = (file, path, message) => errors.push(validationError(file, path, message));
928
+
929
+ const owners = Array.isArray(daemon.owners) ? daemon.owners.map((owner) => String(owner).trim()) : [];
930
+ if (!owners.length || owners.some((owner) => !owner)) push('daemon', 'owners', t('basicSettings.ownerRequired'));
931
+ const ownerSet = new Set();
932
+ owners.forEach((owner) => {
933
+ if (owner && !isValidAid(owner)) push('daemon', 'owners', t('basicSettings.invalidAid'));
934
+ if (owner && ownerSet.has(owner)) push('daemon', 'owners', t('basicSettings.duplicateValue'));
935
+ ownerSet.add(owner);
936
+ });
937
+
938
+ const detected = new Set((state.snapshot?.baseagents || []).filter((item) => item.detected).map((item) => item.id));
939
+ if (!defaults.active_baseagent || !detected.has(defaults.active_baseagent)) push('defaults', 'active_baseagent', t('basicSettings.activeRequired'));
940
+
941
+ if (daemon.aun?.defaultAidDomain !== undefined && !isValidDomain(String(daemon.aun.defaultAidDomain).trim())) {
942
+ push('daemon', 'aun.defaultAidDomain', t('basicSettings.invalidDomain'));
943
+ }
944
+ if (daemon.aun?.minEvolVersion !== undefined && !/^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(String(daemon.aun.minEvolVersion).trim())) {
945
+ push('daemon', 'aun.minEvolVersion', t('basicSettings.invalidSemver'));
946
+ }
947
+ if (defaults.projects?.defaultPath !== undefined) {
948
+ const value = String(defaults.projects.defaultPath).trim();
949
+ if (!/^(?:\/|[A-Za-z]:[\\/]|\\\\)/.test(value)) push('defaults', 'projects.defaultPath', t('basicSettings.invalidPath'));
950
+ }
951
+
952
+ const services = Array.isArray(daemon.services) ? daemon.services : [];
953
+ const serviceNames = new Set();
954
+ services.forEach((service, index) => {
955
+ const base = `services[${index}]`;
956
+ const name = String(service?.name ?? '').trim();
957
+ if (!/^[a-z0-9_-]+$/.test(name)) push('daemon', 'services', `${base}.name: ${t('basicSettings.invalidServiceName')}`);
958
+ if (name && serviceNames.has(name)) push('daemon', 'services', `${base}.name: ${t('basicSettings.duplicateService')}`);
959
+ serviceNames.add(name);
960
+ if (service?.port !== undefined && (!Number.isInteger(service.port) || service.port < 1 || service.port > 65535)) {
961
+ push('daemon', 'services', `${base}.port: ${t('basicSettings.invalidNumber')}`);
962
+ }
963
+ const proxy = service?.proxy;
964
+ if (proxy && typeof proxy === 'object' && !Array.isArray(proxy)) {
965
+ if (proxy.source !== undefined && !['instance', 'static'].includes(proxy.source)) {
966
+ push('daemon', 'services', `${base}.proxy.source: ${t('basicSettings.invalidServiceName')}`);
967
+ }
968
+ if (proxy.serviceType !== undefined && !['http', 'websocket', 'ssh'].includes(proxy.serviceType)) {
969
+ push('daemon', 'services', `${base}.proxy.serviceType: ${t('basicSettings.invalidServiceName')}`);
970
+ }
971
+ if (proxy.visibility !== undefined && !['public', 'private', 'aun-auth'].includes(proxy.visibility)) {
972
+ push('daemon', 'services', `${base}.proxy.visibility: ${t('basicSettings.invalidServiceName')}`);
973
+ }
974
+ if (proxy.endpoint !== undefined && !isServiceEndpoint(String(proxy.endpoint).trim())) {
975
+ push('daemon', 'services', `${base}.proxy.endpoint: ${t('basicSettings.invalidUrl')}`);
976
+ }
977
+ if (proxy.enabled === true && (!['instance', 'static'].includes(proxy.source) || !['public', 'private', 'aun-auth'].includes(proxy.visibility))) {
978
+ push('daemon', 'services', `${base}.proxy: ${t('basicSettings.invalidServiceName')}`);
979
+ }
980
+ if (proxy.enabled === true && proxy.source === 'static' && !String(proxy.endpoint || '').trim()) {
981
+ push('daemon', 'services', `${base}.proxy.endpoint: ${t('basicSettings.valuePlaceholder')}`);
982
+ }
983
+ if (proxy.metadata !== undefined && (typeof proxy.metadata !== 'object' || Array.isArray(proxy.metadata))) {
984
+ push('daemon', 'services', `${base}.proxy.metadata: ${t('basicSettings.invalidServiceName')}`);
985
+ } else if (proxy.metadata && typeof proxy.metadata === 'object') {
986
+ for (const [key, value] of Object.entries(proxy.metadata)) {
987
+ if (!key.trim() || key.length > 256 || typeof value !== 'string' || value.length > 8192 || hasSensitiveMetadataKey(key)) {
988
+ push('daemon', 'services', `${base}.proxy.metadata: ${t('basicSettings.invalidServiceName')}`);
989
+ break;
990
+ }
991
+ }
992
+ }
993
+ }
994
+ });
995
+
996
+ const numberChecks = [
997
+ ['daemon', 'idleMonitor.timeout', daemon.idleMonitor?.timeout, 0, false],
998
+ ['daemon', 'idleMonitor.apiRetryTimeout', daemon.idleMonitor?.apiRetryTimeout, 0, false],
999
+ ['daemon', 'idleMonitor.maxExecutionTime', daemon.idleMonitor?.maxExecutionTime, 0, true],
1000
+ ['defaults', 'session_renew.after_hours', defaults.session_renew?.after_hours, 0, false],
1001
+ ];
1002
+ numberChecks.forEach(([file, path, value, min, inclusive]) => {
1003
+ if (value === undefined) return;
1004
+ const valid = typeof value === 'number' && Number.isFinite(value) && (inclusive ? value >= min : value > min);
1005
+ if (!valid) push(file, path, t('basicSettings.invalidNumber'));
1006
+ });
1007
+
1008
+ const agentBlocks = defaults.baseagents && typeof defaults.baseagents === 'object' ? defaults.baseagents : {};
1009
+ for (const [agent, config] of Object.entries(agentBlocks)) {
1010
+ if (!config || typeof config !== 'object') continue;
1011
+ const base = `baseagents.${agent}`;
1012
+ if (config.baseUrl !== undefined && !isHttpUrl(String(config.baseUrl).trim())) push('defaults', `${base}.baseUrl`, t('basicSettings.invalidUrl'));
1013
+ if (config.apiKey !== undefined && !String(config.apiKey).trim()) push('defaults', `${base}.apiKey`, t('basicSettings.modelRequired'));
1014
+ if (config.effort !== undefined && !effortOptions.includes(String(config.effort))) push('defaults', `${base}.effort`, t('basicSettings.invalidNumber'));
1015
+ if (config.auxiliaryEffort !== undefined && config.auxiliaryModel === undefined) push('defaults', `${base}.auxiliaryEffort`, t('basicSettings.auxiliaryDependency'));
1016
+ if (config.fallbackModels !== undefined) {
1017
+ if (!Array.isArray(config.fallbackModels) || config.fallbackModels.length > 3) push('defaults', `${base}.fallbackModels`, t('basicSettings.fallbackLimit'));
1018
+ const models = new Set();
1019
+ (Array.isArray(config.fallbackModels) ? config.fallbackModels : []).forEach((item) => {
1020
+ const model = String(item?.model ?? '').trim();
1021
+ if (!model) push('defaults', `${base}.fallbackModels`, t('basicSettings.modelRequired'));
1022
+ if (model && models.has(model)) push('defaults', `${base}.fallbackModels`, t('basicSettings.duplicateValue'));
1023
+ models.add(model);
1024
+ if (item?.effort !== undefined && !effortOptions.includes(String(item.effort))) push('defaults', `${base}.fallbackModels`, t('basicSettings.invalidNumber'));
1025
+ });
1026
+ }
1027
+ if (agent === 'ecagent') {
1028
+ [['timeoutMs', 1, 3_600_000], ['maxRetries', 0, 100], ['maxRetryDelayMs', 0, 3_600_000]].forEach(([field, min, max]) => {
1029
+ const value = config[field];
1030
+ if (value !== undefined && (!Number.isInteger(value) || value < min || value > max)) push('defaults', `${base}.${field}`, t('basicSettings.invalidNumber'));
1031
+ });
1032
+ }
1033
+ }
1034
+
1035
+ for (const [file, changes] of Object.entries(state.changes)) {
1036
+ for (const [path, item] of changes) {
1037
+ if (item.op === 'set' && typeof item.value === 'string' && !item.value.trim()) {
1038
+ push(file, path, t('basicSettings.valuePlaceholder'));
1039
+ }
1040
+ }
1041
+ }
1042
+
1043
+ for (const [draftKey, raw] of state.mapDrafts) {
1044
+ const separator = draftKey.indexOf(':');
1045
+ const file = draftKey.slice(0, separator);
1046
+ const path = draftKey.slice(separator + 1);
1047
+ try { parseKeyValueText(raw, path); }
1048
+ catch (error) { push(file, path, error.message); }
1049
+ }
1050
+ for (const raw of state.serviceMetadataDrafts.values()) {
1051
+ try { parseKeyValueText(raw, 'metadata'); }
1052
+ catch (error) { push('daemon', 'services', error.message); }
1053
+ }
1054
+ return errors;
1055
+ }
1056
+
1057
+ function showValidationErrors(errors) {
1058
+ const first = errors[0];
1059
+ state.notice = t('basicSettings.validationFailed', { value: first.message });
1060
+ state.noticeType = 'error';
1061
+ state.error = '';
1062
+ const tab = tabForValidation(first);
1063
+ if (state.tab !== tab) setTab(tab);
1064
+ errors.forEach((error) => setError(findFieldForValidation(error), error.message));
1065
+ updateBasicSettingsStatus();
1066
+ findFieldForValidation(first)?.querySelector('input, select, textarea')?.focus();
1067
+ return false;
1068
+ }
1069
+
1070
+ async function load(force) {
1071
+ if (state.loading || (!force && state.snapshot)) return;
1072
+ if (force && state.changes.daemon.size + state.changes.defaults.size > 0 && !window.confirm(t('basicSettings.reloadConfirm'))) return;
1073
+ state.loading = true; state.error = ''; render();
1074
+ try {
1075
+ const response = await fetch(authApiUrl('api/basic-settings'));
1076
+ const payload = await response.json();
1077
+ if (!response.ok || !payload.success) throw new Error(payload.error || t('basicSettings.loadFailed'));
1078
+ state.snapshot = payload.data;
1079
+ state.changes.daemon.clear(); state.changes.defaults.clear(); state.mapDrafts.clear(); state.serviceMetadataDrafts.clear(); state.restartPaths = []; clearNotice();
1080
+ const detected = (state.snapshot.baseagents || []).filter((item) => item.detected);
1081
+ state.openAgents = new Set(detected.length ? [getFileValue('defaults', 'active_baseagent') || detected[0].id] : []);
1082
+ state.loading = false;
1083
+ render();
1084
+ await loadCatalogs(detected);
1085
+ } catch (error) {
1086
+ state.error = error?.message || String(error);
1087
+ state.loading = false;
1088
+ render();
1089
+ } finally {
1090
+ state.loading = false;
1091
+ if (state.snapshot) render();
1092
+ }
1093
+ }
1094
+
1095
+ async function loadCatalogs(agents) {
1096
+ await Promise.all(agents.map(async (item) => {
1097
+ try {
1098
+ const response = await fetch(authApiUrl(`api/models/catalog?baseagent=${encodeURIComponent(item.id)}`));
1099
+ const payload = await response.json();
1100
+ if (response.ok && payload.success) state.catalogs[item.id] = payload.data.models || [];
1101
+ } catch {}
1102
+ }));
1103
+ if (state.snapshot && !state.loading) render();
1104
+ }
1105
+
1106
+ async function redetect() {
1107
+ if (!state.snapshot || state.detecting) return;
1108
+ state.detecting = true;
1109
+ state.detectionError = '';
1110
+ render();
1111
+ try {
1112
+ const response = await fetch(authApiUrl('api/basic-settings'));
1113
+ const payload = await response.json();
1114
+ if (!response.ok || !payload.success) throw new Error(payload.error || t('basicSettings.loadFailed'));
1115
+ const next = payload.data;
1116
+ state.snapshot.baseagents = next.baseagents || [];
1117
+ state.snapshot.logTypes = next.logTypes || [];
1118
+ state.snapshot.logTypeCounts = next.logTypeCounts || {};
1119
+ const detected = state.snapshot.baseagents.filter((item) => item.detected);
1120
+ await loadCatalogs(detected);
1121
+ } catch (error) {
1122
+ state.detectionError = error?.message || String(error);
1123
+ } finally {
1124
+ state.detecting = false;
1125
+ render();
1126
+ }
1127
+ }
1128
+
1129
+ function open() {
1130
+ const modal = byId('basic-settings-modal');
1131
+ if (!modal) return;
1132
+ modal.hidden = false;
1133
+ document.body.classList.add('basic-settings-open');
1134
+ load(false);
1135
+ byId('basic-settings-close')?.focus();
1136
+ }
1137
+
1138
+ function close(force) {
1139
+ if (!force && state.changes.daemon.size + state.changes.defaults.size > 0 && !window.confirm(t('basicSettings.closeConfirm'))) return;
1140
+ const modal = byId('basic-settings-modal');
1141
+ if (modal) modal.hidden = true;
1142
+ document.body.classList.remove('basic-settings-open');
1143
+ }
1144
+
1145
+ function controlValue(control) {
1146
+ const type = control.dataset.valueType;
1147
+ if (type === 'boolean') return control.value === 'true';
1148
+ if (type === 'number') return control.value === '' ? undefined : Number(control.value);
1149
+ return control.value;
1150
+ }
1151
+
1152
+ function numericPresenceDefault(path) {
1153
+ if (path === 'idleMonitor.apiRetryTimeout') return Number(getFileValue('daemon', 'idleMonitor.timeout')) || 120;
1154
+ const defaults = {
1155
+ 'idleMonitor.timeout': 120,
1156
+ 'idleMonitor.maxExecutionTime': 0,
1157
+ 'session_renew.after_hours': 24,
1158
+ 'baseagents.ecagent.timeoutMs': 300000,
1159
+ 'baseagents.ecagent.maxRetries': 2,
1160
+ 'baseagents.ecagent.maxRetryDelayMs': 8000,
1161
+ };
1162
+ return defaults[path] ?? 0;
1163
+ }
1164
+
1165
+ function applyPresence(input) {
1166
+ const field = input.closest('[data-bs-field]');
1167
+ if (!field) return;
1168
+ const control = field.querySelector('[data-bs-control], [data-bs-map]');
1169
+ if (!input.checked) {
1170
+ recordChange(input.dataset.file, input.dataset.path, 'unset');
1171
+ render();
1172
+ return;
1173
+ }
1174
+ if (!control) return;
1175
+ if (control.matches('[data-bs-map]')) {
1176
+ try {
1177
+ const value = parseKeyValueText(control.value, input.dataset.path);
1178
+ state.mapDrafts.set(`${input.dataset.file}:${input.dataset.path}`, control.value);
1179
+ recordChange(input.dataset.file, input.dataset.path, 'set', value);
1180
+ setError(field, '');
1181
+ render();
1182
+ } catch (error) {
1183
+ input.checked = false;
1184
+ setError(field, error.message);
1185
+ }
1186
+ return;
1187
+ }
1188
+ let value = control.dataset.valueType === 'number'
1189
+ ? (control.value === '' ? undefined : Number(control.value))
1190
+ : control.dataset.valueType === 'boolean' ? (control.value === '' ? undefined : control.value === 'true') : control.value;
1191
+ if (value === undefined && control.dataset.valueType === 'number') {
1192
+ const suggested = Number(control.placeholder);
1193
+ value = Number.isFinite(suggested) ? suggested : numericPresenceDefault(input.dataset.path);
1194
+ }
1195
+ recordChange(input.dataset.file, input.dataset.path, 'set', value ?? '');
1196
+ render();
1197
+ }
1198
+
1199
+ function collectOwners() {
1200
+ const values = [...document.querySelectorAll('[data-bs-owner]')].map((input) => input.value.trim());
1201
+ recordChange('daemon', 'owners', 'set', values);
1202
+ }
1203
+
1204
+ function collectServices() {
1205
+ const previous = Array.isArray(buildFilePreview('daemon').services) ? buildFilePreview('daemon').services : [];
1206
+ const values = [...document.querySelectorAll('[data-bs-service]')].map((row) => {
1207
+ const index = Number(row.dataset.index);
1208
+ const service = clone(previous[index] || { name: '', enabled: true });
1209
+ const hadProxy = isObject(service.proxy);
1210
+ service.proxy = isObject(service.proxy) ? service.proxy : {};
1211
+ const metadataPresence = row.querySelector('[data-bs-service-metadata-presence]');
1212
+ if (metadataPresence && !metadataPresence.checked) {
1213
+ delete service.proxy.metadata;
1214
+ }
1215
+ row.querySelectorAll('[data-bs-service-field]').forEach((control) => {
1216
+ const field = control.dataset.bsServiceField;
1217
+ const value = control.value;
1218
+ if (field === 'proxy.metadata' && metadataPresence && !metadataPresence.checked) return;
1219
+ if (field === 'enabled' || field === 'proxy.enabled') {
1220
+ if (field.startsWith('proxy.')) {
1221
+ service.proxy = isObject(service.proxy) ? service.proxy : {};
1222
+ service.proxy.enabled = value === 'true';
1223
+ } else service.enabled = value === 'true';
1224
+ } else if (field === 'port') {
1225
+ if (value === '') delete service.port;
1226
+ else service.port = Number(value);
1227
+ } else if (field === 'proxy.metadata') {
1228
+ try {
1229
+ service.proxy.metadata = parseKeyValueText(value, 'metadata');
1230
+ setError(control.closest('[data-bs-service-metadata-field]'), '');
1231
+ } catch (error) {
1232
+ setError(control.closest('[data-bs-service-metadata-field]'), error.message);
1233
+ }
1234
+ } else if (field.startsWith('proxy.')) {
1235
+ service.proxy[field.slice(6)] = value;
1236
+ } else service[field] = value;
1237
+ });
1238
+ if (service.proxy.enabled === true) {
1239
+ service.proxy.source ||= 'instance';
1240
+ service.proxy.serviceType ||= 'http';
1241
+ service.proxy.visibility ||= 'private';
1242
+ }
1243
+ if (service.proxy.source === 'instance') delete service.proxy.endpoint;
1244
+ if (!hadProxy && service.proxy.enabled !== true && (!service.proxy.source || service.proxy.source === 'instance')
1245
+ && (!service.proxy.serviceType || service.proxy.serviceType === 'http') && service.proxy.visibility === 'private'
1246
+ && (!service.proxy.metadata || Object.keys(service.proxy.metadata).length === 0)) delete service.proxy;
1247
+ return service;
1248
+ });
1249
+ recordChange('daemon', 'services', 'set', values);
1250
+ }
1251
+
1252
+ function collectFallback(row) {
1253
+ const agent = row.dataset.agent;
1254
+ const values = [...document.querySelectorAll(`[data-bs-fallback-row][data-agent="${CSS.escape(agent)}"]`)].map((item) => ({
1255
+ model: item.querySelector('[data-bs-fallback-model]')?.value || '',
1256
+ effort: item.querySelector('[data-bs-fallback-effort]')?.value || undefined,
1257
+ }));
1258
+ recordChange('defaults', `baseagents.${agent}.fallbackModels`, 'set', values);
1259
+ }
1260
+
1261
+ function collectLogTypes() {
1262
+ const values = [...document.querySelectorAll('#basic-settings-log-types input[data-bs-log-type]:checked')]
1263
+ .map(input => input.value)
1264
+ .filter(value => typeof value === 'string' && value.trim());
1265
+ recordChange('daemon', 'watch.logTypes', 'set', [...new Set(values)]);
1266
+ }
1267
+
1268
+ function addFallback(agent) {
1269
+ const current = getFileValue('defaults', `baseagents.${agent}.fallbackModels`);
1270
+ const rows = Array.isArray(current) ? current.slice() : [];
1271
+ if (rows.length >= 3) {
1272
+ state.notice = t('basicSettings.fallbackLimit');
1273
+ state.noticeType = 'error';
1274
+ updateBasicSettingsStatus();
1275
+ return;
1276
+ }
1277
+ rows.push({ model: state.catalogs[agent]?.[0]?.id || '', effort: 'medium' });
1278
+ recordChange('defaults', `baseagents.${agent}.fallbackModels`, 'set', rows);
1279
+ render();
1280
+ }
1281
+
1282
+ function removeFallback(row) {
1283
+ const agent = row.dataset.agent;
1284
+ const index = Number(row.dataset.index);
1285
+ const rows = Array.isArray(getFileValue('defaults', `baseagents.${agent}.fallbackModels`)) ? getFileValue('defaults', `baseagents.${agent}.fallbackModels`).slice() : [];
1286
+ rows.splice(index, 1);
1287
+ recordChange('defaults', `baseagents.${agent}.fallbackModels`, 'set', rows);
1288
+ render();
1289
+ }
1290
+
1291
+ function addService() {
1292
+ const values = Array.isArray(getFileValue('daemon', 'services')) ? getFileValue('daemon', 'services').slice() : [];
1293
+ values.push({ name: `service-${values.length + 1}`, enabled: true });
1294
+ recordChange('daemon', 'services', 'set', values); render();
1295
+ }
1296
+
1297
+ function removeService(row) {
1298
+ const index = Number(row.dataset.index);
1299
+ const values = Array.isArray(getFileValue('daemon', 'services')) ? getFileValue('daemon', 'services').slice() : [];
1300
+ values.splice(index, 1);
1301
+ // Service metadata drafts are indexed by row. Remove the deleted row and
1302
+ // reindex following drafts so they remain associated with their service.
1303
+ const nextMetadataDrafts = new Map();
1304
+ for (const [key, value] of state.serviceMetadataDrafts) {
1305
+ const draftIndex = Number(key);
1306
+ if (!Number.isInteger(draftIndex) || draftIndex === index) continue;
1307
+ nextMetadataDrafts.set(String(draftIndex > index ? draftIndex - 1 : draftIndex), value);
1308
+ }
1309
+ state.serviceMetadataDrafts = nextMetadataDrafts;
1310
+ const nextEditingNames = new Set();
1311
+ for (const key of state.editingServiceNames) {
1312
+ const editingIndex = Number(key);
1313
+ if (!Number.isInteger(editingIndex) || editingIndex === index) continue;
1314
+ nextEditingNames.add(String(editingIndex > index ? editingIndex - 1 : editingIndex));
1315
+ }
1316
+ state.editingServiceNames = nextEditingNames;
1317
+ recordChange('daemon', 'services', 'set', values); render();
1318
+ }
1319
+
1320
+ function addOwner() {
1321
+ const values = Array.isArray(getFileValue('daemon', 'owners')) ? getFileValue('daemon', 'owners').slice() : [];
1322
+ values.push(''); recordChange('daemon', 'owners', 'set', values); render();
1323
+ document.querySelector('[data-bs-owner]:last-of-type')?.focus();
1324
+ }
1325
+
1326
+ function removeOwner(row) {
1327
+ const values = Array.isArray(getFileValue('daemon', 'owners')) ? getFileValue('daemon', 'owners').slice() : [];
1328
+ const rows = [...document.querySelectorAll('[data-bs-owner-row]')];
1329
+ values.splice(rows.indexOf(row), 1); recordChange('daemon', 'owners', 'set', values); render();
1330
+ }
1331
+
1332
+ function setTab(tab) {
1333
+ state.tab = tab;
1334
+ document.querySelectorAll('[data-basic-tab]').forEach((button) => {
1335
+ const active = button.dataset.basicTab === tab;
1336
+ button.classList.toggle('active', active);
1337
+ button.setAttribute('aria-selected', String(active));
1338
+ });
1339
+ render();
1340
+ }
1341
+
1342
+ function refreshI18n() {
1343
+ if (byId('basic-settings-modal')?.hidden !== false) return;
1344
+ if (state.noticeType === 'success') state.notice = savedNotice();
1345
+ render();
1346
+ }
1347
+ window.refreshBasicSettingsI18n = refreshI18n;
1348
+
1349
+ function savedNotice() {
1350
+ return state.restartPaths.length
1351
+ ? `${t('basicSettings.restartNotice')} ${t('basicSettings.restartPaths', { value: state.restartPaths.join(', ') })}`
1352
+ : t('basicSettings.saved');
1353
+ }
1354
+
1355
+ async function readApiJson(response, fallbackMessage) {
1356
+ const raw = await response.text();
1357
+ try {
1358
+ return raw.trim() ? JSON.parse(raw) : {};
1359
+ } catch {
1360
+ throw new Error(`${fallbackMessage}${response.status ? ` (${response.status})` : ''}`);
1361
+ }
1362
+ }
1363
+
1364
+ async function save() {
1365
+ if (!state.snapshot || state.saving) return;
1366
+ const files = ['daemon', 'defaults'].filter((file) => state.changes[file].size > 0);
1367
+ if (!files.length) return;
1368
+ const validationErrors = validateBeforeSave();
1369
+ if (validationErrors.length) { showValidationErrors(validationErrors); return; }
1370
+ state.saving = true;
1371
+ const button = byId('basic-settings-save');
1372
+ if (button) button.disabled = true;
1373
+ try {
1374
+ const restartPaths = [];
1375
+ for (const file of files) {
1376
+ const changes = [...state.changes[file]].map(([path, item]) => ({ path, ...item }));
1377
+ const response = await fetch(authApiUrl('api/basic-settings'), {
1378
+ method: 'PATCH',
1379
+ headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${localStorage.getItem(TOKEN_KEY) || ''}` },
1380
+ body: JSON.stringify({ file, revision: state.snapshot.revisions[file], changes }),
1381
+ });
1382
+ const payload = await response.json();
1383
+ if (!response.ok || !payload.success) throw new Error(payload.error || t('basicSettings.saveFailed'));
1384
+ state.snapshot = payload.data;
1385
+ state.changes[file].clear();
1386
+ if (payload.restartRequired) restartPaths.push(...(payload.restartPaths || []));
1387
+ }
1388
+ state.restartPaths = [...new Set(restartPaths)];
1389
+ state.mapDrafts.clear();
1390
+ state.serviceMetadataDrafts.clear();
1391
+ state.error = '';
1392
+ state.notice = savedNotice();
1393
+ state.noticeType = 'success';
1394
+ render();
1395
+ } catch (error) {
1396
+ state.error = '';
1397
+ state.notice = error?.message || String(error);
1398
+ state.noticeType = 'error';
1399
+ updateBasicSettingsStatus();
1400
+ } finally {
1401
+ state.saving = false;
1402
+ updateBasicSettingsStatus();
1403
+ }
1404
+ }
1405
+
1406
+ function showJson(file) {
1407
+ const modal = byId('basic-settings-json-modal');
1408
+ const view = byId('basic-settings-json-view');
1409
+ if (!modal || !view) return;
1410
+ document.querySelectorAll('[data-basic-json-file]').forEach((button) => button.classList.toggle('active', button.dataset.basicJsonFile === file));
1411
+ view.textContent = JSON.stringify(redactJson(buildFilePreview(file), file), null, 2);
1412
+ modal.hidden = false;
1413
+ }
1414
+
1415
+ function handleContentEvent(event) {
1416
+ const target = event.target;
1417
+ if (target.matches('[data-secret-mask]') && event.type === 'focusin' && target.value === '******') {
1418
+ target.value = '';
1419
+ return;
1420
+ }
1421
+ if (target.matches('[data-bs-service-name-input]') && event.type === 'focusout') {
1422
+ if (event.relatedTarget?.closest?.('[data-bs-edit-service-name]')
1423
+ && event.relatedTarget.closest('[data-bs-service]') === target.closest('[data-bs-service]')) return;
1424
+ collectServices();
1425
+ state.editingServiceNames.delete(String(target.closest('[data-bs-service]')?.dataset.index));
1426
+ setTimeout(render, 0);
1427
+ return;
1428
+ }
1429
+ if (target.matches('[data-bs-presence]') && event.type === 'change') return applyPresence(target);
1430
+ if (target.matches('[data-bs-service-metadata-presence]') && event.type === 'change') {
1431
+ const index = String(target.dataset.bsServiceIndex || '');
1432
+ if (!target.checked) state.serviceMetadataDrafts.delete(index);
1433
+ collectServices();
1434
+ render();
1435
+ return;
1436
+ }
1437
+ if (target.matches('[data-bs-active-baseagent]') && event.type === 'change') { recordChange('defaults', 'active_baseagent', 'set', target.value); render(); return; }
1438
+ if (target.matches('[data-bs-control]') && (event.type === 'change' || event.type === 'input')) {
1439
+ if (target.dataset.sensitive === 'true' && target.value === '' && hasAt(state.snapshot?.[target.dataset.file], target.dataset.path)) {
1440
+ state.changes[target.dataset.file].delete(target.dataset.path);
1441
+ updateBasicSettingsStatus();
1442
+ return;
1443
+ }
1444
+ const value = controlValue(target);
1445
+ if (value === undefined) recordChange(target.dataset.file, target.dataset.path, 'unset');
1446
+ else recordChange(target.dataset.file, target.dataset.path, 'set', value);
1447
+ if (target.dataset.path === 'config.chatMode' && event.type === 'change') render();
1448
+ return;
1449
+ }
1450
+ if (target.matches('[data-bs-map]:not([data-bs-service-map])') && event.type === 'input') {
1451
+ const field = target.closest('[data-bs-field]');
1452
+ state.mapDrafts.set(`${target.dataset.file}:${target.dataset.path}`, target.value);
1453
+ try { recordChange(target.dataset.file, target.dataset.path, 'set', parseKeyValueText(target.value, target.dataset.path)); setError(field, ''); }
1454
+ catch (error) { setError(field, error.message); }
1455
+ return;
1456
+ }
1457
+ if (target.matches('[data-bs-log-type]') && event.type === 'change') { collectLogTypes(); return; }
1458
+ if (target.matches('[data-bs-owner]') && event.type === 'input') { collectOwners(); return; }
1459
+ if (target.matches('[data-bs-service-field]') && (event.type === 'change' || event.type === 'input')) {
1460
+ if (target.dataset.bsServiceField === 'proxy.metadata') state.serviceMetadataDrafts.set(String(target.dataset.bsServiceIndex), target.value);
1461
+ collectServices();
1462
+ if (event.type === 'change' && ['proxy.source', 'proxy.enabled'].includes(target.dataset.bsServiceField)) render();
1463
+ return;
1464
+ }
1465
+ if (target.matches('[data-bs-fallback-model], [data-bs-fallback-effort]') && event.type === 'change') { collectFallback(target.closest('[data-bs-fallback-row]')); return; }
1466
+ }
1467
+
1468
+ function handleClick(event) {
1469
+ const target = event.target;
1470
+ const secretToggle = target.closest('[data-bs-toggle-secret]');
1471
+ if (secretToggle) {
1472
+ const input = secretToggle.parentElement?.querySelector('[data-sensitive="true"]');
1473
+ if (input) {
1474
+ const file = secretToggle.dataset.file || input.dataset.file || 'defaults';
1475
+ const fieldPath = secretToggle.dataset.path || input.dataset.path || '';
1476
+ const pending = state.changes[file]?.has(fieldPath);
1477
+ const visible = input.dataset.secretRevealed === 'true';
1478
+ if (visible) {
1479
+ input.type = 'password';
1480
+ input.dataset.secretRevealed = 'false';
1481
+ if (!pending) {
1482
+ input.value = '******';
1483
+ input.dataset.secretMask = 'true';
1484
+ }
1485
+ const label = t('basicSettings.showSecret');
1486
+ secretToggle.setAttribute('aria-label', label);
1487
+ secretToggle.setAttribute('title', label);
1488
+ return;
1489
+ }
1490
+ if (pending || input.value !== '******') {
1491
+ input.type = 'text';
1492
+ input.dataset.secretRevealed = 'true';
1493
+ delete input.dataset.secretMask;
1494
+ const label = t('basicSettings.hideSecret');
1495
+ secretToggle.setAttribute('aria-label', label);
1496
+ secretToggle.setAttribute('title', label);
1497
+ return;
1498
+ }
1499
+ secretToggle.disabled = true;
1500
+ fetch(authApiUrl(`api/basic-settings/secret?file=${encodeURIComponent(file)}&path=${encodeURIComponent(fieldPath)}`))
1501
+ .then(async (response) => {
1502
+ const payload = await readApiJson(response, t('basicSettings.revealSecretFailed'));
1503
+ if (!response.ok || !payload.success || typeof payload.data?.value !== 'string') {
1504
+ throw new Error(payload.error || t('basicSettings.revealSecretFailed'));
1505
+ }
1506
+ input.value = payload.data.value;
1507
+ input.type = 'text';
1508
+ input.dataset.secretRevealed = 'true';
1509
+ delete input.dataset.secretMask;
1510
+ const label = t('basicSettings.hideSecret');
1511
+ secretToggle.setAttribute('aria-label', label);
1512
+ secretToggle.setAttribute('title', label);
1513
+ })
1514
+ .catch((error) => {
1515
+ state.notice = error?.message || String(error);
1516
+ state.noticeType = 'error';
1517
+ updateBasicSettingsStatus();
1518
+ })
1519
+ .finally(() => { secretToggle.disabled = false; });
1520
+ }
1521
+ return;
1522
+ }
1523
+ const tab = target.closest('[data-basic-tab]');
1524
+ if (tab) { setTab(tab.dataset.basicTab); return; }
1525
+ if (target.closest('[data-bs-redetect]')) { redetect(); return; }
1526
+ if (target.closest('[data-bs-refresh]')) { load(true); return; }
1527
+ const agentToggle = target.closest('[data-bs-agent-toggle]');
1528
+ if (agentToggle && !target.matches('input')) {
1529
+ const card = agentToggle.closest('[data-bs-agent]');
1530
+ if (card) { const id = card.dataset.bsAgent; if (state.openAgents.has(id)) state.openAgents.delete(id); else state.openAgents.add(id); render(); }
1531
+ return;
1532
+ }
1533
+ if (target.closest('[data-bs-add-fallback]')) { addFallback(target.closest('[data-bs-add-fallback]').dataset.agent); return; }
1534
+ if (target.closest('[data-bs-remove-fallback]')) { removeFallback(target.closest('[data-bs-fallback-row]')); return; }
1535
+ if (target.closest('[data-bs-add-service]')) { addService(); return; }
1536
+ if (target.closest('[data-bs-remove-service]')) { removeService(target.closest('[data-bs-service]')); return; }
1537
+ const editServiceName = target.closest('[data-bs-edit-service-name]');
1538
+ if (editServiceName) {
1539
+ const index = String(editServiceName.dataset.bsServiceIndex);
1540
+ if (state.editingServiceNames.has(index)) {
1541
+ collectServices();
1542
+ state.editingServiceNames.delete(index);
1543
+ render();
1544
+ } else {
1545
+ state.editingServiceNames.add(index);
1546
+ render();
1547
+ document.querySelector(`[data-bs-service][data-index="${CSS.escape(index)}"] [data-bs-service-name-input]`)?.focus();
1548
+ }
1549
+ return;
1550
+ }
1551
+ if (target.closest('[data-bs-add-owner]')) { addOwner(); return; }
1552
+ if (target.closest('[data-bs-remove-owner]')) { removeOwner(target.closest('[data-bs-owner-row]')); return; }
1553
+ }
1554
+
1555
+ function init() {
1556
+ byId('basic-settings-btn')?.addEventListener('click', open);
1557
+ byId('basic-settings-close')?.addEventListener('click', () => close(false));
1558
+ byId('basic-settings-reload')?.addEventListener('click', () => load(true));
1559
+ byId('basic-settings-save')?.addEventListener('click', save);
1560
+ byId('basic-settings-json')?.addEventListener('click', () => showJson('daemon'));
1561
+ byId('basic-settings-json-close')?.addEventListener('click', () => { byId('basic-settings-json-modal').hidden = true; });
1562
+ byId('basic-settings-json-modal')?.addEventListener('click', (event) => { if (event.target === event.currentTarget) event.currentTarget.hidden = true; });
1563
+ document.querySelectorAll('[data-basic-json-file]').forEach((button) => button.addEventListener('click', () => showJson(button.dataset.basicJsonFile)));
1564
+ byId('basic-settings-content')?.addEventListener('input', handleContentEvent);
1565
+ byId('basic-settings-content')?.addEventListener('change', handleContentEvent);
1566
+ byId('basic-settings-content')?.addEventListener('focusin', handleContentEvent);
1567
+ byId('basic-settings-content')?.addEventListener('focusout', handleContentEvent);
1568
+ byId('basic-settings-content')?.addEventListener('click', handleClick);
1569
+ document.querySelector('.basic-settings-tabs')?.addEventListener('click', handleClick);
1570
+ byId('basic-settings-modal')?.addEventListener('click', (event) => { if (event.target === event.currentTarget) close(false); });
1571
+ document.addEventListener('keydown', (event) => { if (event.key === 'Escape' && byId('basic-settings-json-modal')?.hidden === false) byId('basic-settings-json-modal').hidden = true; else if (event.key === 'Escape' && byId('basic-settings-modal')?.hidden === false) close(false); });
1572
+ }
1573
+
1574
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
1575
+ else init();
1576
+ })();