@jungjaehoon/mama-os 0.32.3 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. package/README.md +15 -17
  2. package/dist/agent/os-agent-capabilities.md +2 -2
  3. package/dist/api/graph-api.d.ts +4 -4
  4. package/dist/api/graph-api.d.ts.map +1 -1
  5. package/dist/api/graph-api.js +47 -78
  6. package/dist/api/graph-api.js.map +1 -1
  7. package/dist/api/index.d.ts +10 -0
  8. package/dist/api/index.d.ts.map +1 -1
  9. package/dist/api/index.js +9 -2
  10. package/dist/api/index.js.map +1 -1
  11. package/dist/api/operator-handler.d.ts +1 -1
  12. package/dist/api/operator-handler.js +1 -1
  13. package/dist/api/runtime-status-handler.d.ts +41 -0
  14. package/dist/api/runtime-status-handler.d.ts.map +1 -0
  15. package/dist/api/runtime-status-handler.js +55 -0
  16. package/dist/api/runtime-status-handler.js.map +1 -0
  17. package/dist/cli/commands/start.d.ts.map +1 -1
  18. package/dist/cli/commands/start.js +58 -23
  19. package/dist/cli/commands/start.js.map +1 -1
  20. package/dist/cli/runtime/api-routes-init.d.ts +2 -0
  21. package/dist/cli/runtime/api-routes-init.d.ts.map +1 -1
  22. package/dist/cli/runtime/api-routes-init.js +62 -1
  23. package/dist/cli/runtime/api-routes-init.js.map +1 -1
  24. package/dist/cli/runtime/api-server-init.d.ts +12 -1
  25. package/dist/cli/runtime/api-server-init.d.ts.map +1 -1
  26. package/dist/cli/runtime/api-server-init.js +54 -0
  27. package/dist/cli/runtime/api-server-init.js.map +1 -1
  28. package/dist/cli/runtime/utilities.d.ts +1 -1
  29. package/dist/cli/runtime/utilities.d.ts.map +1 -1
  30. package/dist/cli/runtime/utilities.js +3 -1
  31. package/dist/cli/runtime/utilities.js.map +1 -1
  32. package/dist/gateways/message-router.d.ts +4 -0
  33. package/dist/gateways/message-router.d.ts.map +1 -1
  34. package/dist/gateways/message-router.js +62 -13
  35. package/dist/gateways/message-router.js.map +1 -1
  36. package/dist/gateways/owner-report-inbox.d.ts +40 -0
  37. package/dist/gateways/owner-report-inbox.d.ts.map +1 -0
  38. package/dist/gateways/owner-report-inbox.js +128 -0
  39. package/dist/gateways/owner-report-inbox.js.map +1 -0
  40. package/dist/gateways/report-projection-v1.d.ts +41 -0
  41. package/dist/gateways/report-projection-v1.d.ts.map +1 -0
  42. package/dist/gateways/report-projection-v1.js +160 -0
  43. package/dist/gateways/report-projection-v1.js.map +1 -0
  44. package/dist/gateways/session-store.d.ts +22 -0
  45. package/dist/gateways/session-store.d.ts.map +1 -1
  46. package/dist/gateways/session-store.js +76 -3
  47. package/dist/gateways/session-store.js.map +1 -1
  48. package/dist/gateways/telegram-message-ledger.d.ts +10 -0
  49. package/dist/gateways/telegram-message-ledger.d.ts.map +1 -1
  50. package/dist/gateways/telegram-message-ledger.js +23 -2
  51. package/dist/gateways/telegram-message-ledger.js.map +1 -1
  52. package/dist/gateways/telegram-report-context-store.d.ts +193 -0
  53. package/dist/gateways/telegram-report-context-store.d.ts.map +1 -0
  54. package/dist/gateways/telegram-report-context-store.js +612 -0
  55. package/dist/gateways/telegram-report-context-store.js.map +1 -0
  56. package/dist/gateways/telegram.d.ts +9 -0
  57. package/dist/gateways/telegram.d.ts.map +1 -1
  58. package/dist/gateways/telegram.js +100 -0
  59. package/dist/gateways/telegram.js.map +1 -1
  60. package/dist/gateways/types.d.ts +12 -0
  61. package/dist/gateways/types.d.ts.map +1 -1
  62. package/dist/multi-agent/dashboard-agent-persona.js +2 -2
  63. package/dist/operator/board-slot-instructions.d.ts +1 -1
  64. package/dist/operator/board-slot-instructions.js +1 -1
  65. package/dist/operator/operator-trigger-loop.d.ts +17 -1
  66. package/dist/operator/operator-trigger-loop.d.ts.map +1 -1
  67. package/dist/operator/operator-trigger-loop.js +87 -23
  68. package/dist/operator/operator-trigger-loop.js.map +1 -1
  69. package/dist/operator/report-delivery-coordinator.d.ts +82 -0
  70. package/dist/operator/report-delivery-coordinator.d.ts.map +1 -0
  71. package/dist/operator/report-delivery-coordinator.js +123 -0
  72. package/dist/operator/report-delivery-coordinator.js.map +1 -0
  73. package/dist/operator/situation-report.d.ts +7 -0
  74. package/dist/operator/situation-report.d.ts.map +1 -1
  75. package/dist/operator/situation-report.js +12 -0
  76. package/dist/operator/situation-report.js.map +1 -1
  77. package/package.json +1 -2
  78. package/public/viewer/js/modules/system.js +183 -0
  79. package/public/viewer/js/utils/ui-commands.js +4 -9
  80. package/public/viewer/log-viewer.html +2 -192
  81. package/public/viewer/operator/operator.css +1 -0
  82. package/public/viewer/operator/operator.js +14830 -0
  83. package/public/viewer/src/modules/system.ts +242 -0
  84. package/public/viewer/src/utils/ui-commands.ts +4 -8
  85. package/public/viewer/sw.js +44 -10
  86. package/public/viewer/viewer.css +338 -331
  87. package/public/viewer/viewer.html +388 -1472
  88. package/public/ui/assets/index-BDXDGEV6.js +0 -62
  89. package/public/ui/assets/index-po2tN0xq.css +0 -1
  90. package/public/ui/index.html +0 -14
  91. package/public/ui/theme-init.js +0 -20
  92. package/public/viewer/js/modules/agents.js +0 -1153
  93. package/public/viewer/js/modules/chat.js +0 -1993
  94. package/public/viewer/js/modules/connector-feed.js +0 -352
  95. package/public/viewer/js/modules/dashboard.js +0 -456
  96. package/public/viewer/js/modules/entity-audit.js +0 -106
  97. package/public/viewer/js/modules/entity-review.js +0 -66
  98. package/public/viewer/js/modules/settings.js +0 -1257
  99. package/public/viewer/src/modules/agents.ts +0 -1302
  100. package/public/viewer/src/modules/chat.ts +0 -2426
  101. package/public/viewer/src/modules/connector-feed.ts +0 -391
  102. package/public/viewer/src/modules/dashboard.ts +0 -498
  103. package/public/viewer/src/modules/entity-audit.ts +0 -154
  104. package/public/viewer/src/modules/entity-review.ts +0 -175
  105. package/public/viewer/src/modules/settings.ts +0 -1547
@@ -1,1257 +0,0 @@
1
- /**
2
- * Settings Module - MAMA OS Settings Management
3
- * @module modules/settings
4
- * @version 1.0.0
5
- *
6
- * Handles Settings tab functionality including:
7
- * - Load current configuration
8
- * - Save configuration changes
9
- * - Form validation
10
- * - Gateway enable/disable toggles
11
- */
12
- /* eslint-env browser */
13
- import { showToast, escapeHtml, escapeAttr, getElementByIdOrNull } from '../utils/dom.js';
14
- import { formatModelName } from '../utils/format.js';
15
- import { DebugLogger } from '../utils/debug-logger.js';
16
- import { reportPageContext } from '../utils/ui-commands.js';
17
- import { API, } from '../utils/api.js';
18
- const logger = new DebugLogger('Settings');
19
- // Model options by backend (single source of truth)
20
- // Claude models: https://platform.claude.com/docs/en/about-claude/models/overview
21
- const MODEL_OPTIONS = {
22
- codex: [
23
- 'gpt-5.4',
24
- 'gpt-5.4-mini',
25
- 'gpt-4.1',
26
- 'gpt-4o',
27
- 'gpt-4o-mini',
28
- 'o1',
29
- 'o1-mini',
30
- 'o3-mini',
31
- ],
32
- claude: [
33
- // Latest models (4.6)
34
- 'claude-opus-4-6',
35
- 'claude-sonnet-4-6',
36
- // Previous gen (4.5)
37
- 'claude-opus-4-5-20251101',
38
- 'claude-sonnet-4-5-20250929',
39
- 'claude-haiku-4-5-20251001',
40
- // Legacy models
41
- 'claude-sonnet-4-20250514',
42
- 'claude-opus-4-20250514',
43
- 'claude-3-7-sonnet-20250219',
44
- 'claude-3-haiku-20240307',
45
- ],
46
- cline: ['deepseek/deepseek-v4-flash'],
47
- };
48
- const DEFAULT_MODEL = {
49
- claude: 'claude-sonnet-4-6',
50
- codex: 'gpt-5.4',
51
- cline: 'deepseek/deepseek-v4-flash',
52
- };
53
- export function getSettingsModelOptions(backend, currentModel) {
54
- const options = MODEL_OPTIONS[backend] || MODEL_OPTIONS.claude;
55
- return currentModel && !options.includes(currentModel)
56
- ? [...options, currentModel]
57
- : [...options];
58
- }
59
- const EFFORT_SUPPORTED_MODELS = new Set(['claude-opus-4-6', 'claude-sonnet-4-6']);
60
- const MAX_EFFORT_MODELS = new Set(['claude-opus-4-6']);
61
- /**
62
- * Settings Module Class
63
- */
64
- export class SettingsModule {
65
- config = null;
66
- mcpServersData = { servers: [] };
67
- multiAgentData = { agents: [] };
68
- initialized = false;
69
- backendListenersInitialized = false;
70
- delegatedListenersInitialized = false;
71
- constructor() { }
72
- supportsEffortModel(model) {
73
- return EFFORT_SUPPORTED_MODELS.has(model);
74
- }
75
- supportsMaxEffortModel(model) {
76
- return MAX_EFFORT_MODELS.has(model);
77
- }
78
- normalizeEffortForModel(model, effort) {
79
- if (effort === 'max' && !this.supportsMaxEffortModel(model)) {
80
- return 'high';
81
- }
82
- return effort;
83
- }
84
- getEffortLevelsForModel(model) {
85
- if (this.supportsMaxEffortModel(model)) {
86
- return ['low', 'medium', 'high', 'max'];
87
- }
88
- return ['low', 'medium', 'high'];
89
- }
90
- buildEffortOptions(model, selectedEffort) {
91
- return this.getEffortLevelsForModel(model)
92
- .map((effort) => {
93
- const selected = selectedEffort === effort ? ' selected' : '';
94
- const label = effort === 'max' ? `${effort} (Opus)` : effort;
95
- return `<option value="${effort}"${selected}>${label}</option>`;
96
- })
97
- .join('');
98
- }
99
- refreshAgentEffortControls(agentId, model) {
100
- const effortContainer = getElementByIdOrNull(`agent-effort-container-${agentId}`);
101
- if (!effortContainer) {
102
- return;
103
- }
104
- const supportsEffort = this.supportsEffortModel(model);
105
- effortContainer.style.display = supportsEffort ? 'block' : 'none';
106
- const effortSelect = getElementByIdOrNull(`agent-effort-${agentId}`);
107
- if (!effortSelect) {
108
- return;
109
- }
110
- const currentEffort = (effortSelect.value || 'medium');
111
- const normalizedEffort = this.normalizeEffortForModel(model, currentEffort);
112
- effortSelect.innerHTML = this.buildEffortOptions(model, normalizedEffort);
113
- effortSelect.value = normalizedEffort;
114
- }
115
- /**
116
- * Parse and validate required integer input.
117
- */
118
- parseIntegerInput(id, min, max, fallback) {
119
- const raw = this.getValue(id);
120
- if (raw === null) {
121
- if (fallback === null) {
122
- throw new Error(`필수 값이 비어 있습니다: ${id}`);
123
- }
124
- return fallback;
125
- }
126
- const trimmed = raw.trim();
127
- if (!trimmed) {
128
- if (fallback === null) {
129
- throw new Error(`필수 값이 비어 있습니다: ${id}`);
130
- }
131
- return fallback;
132
- }
133
- const parsed = Number(trimmed);
134
- if (!Number.isFinite(parsed) || !Number.isInteger(parsed)) {
135
- throw new Error(`숫자 형식이 유효하지 않습니다: ${id}`);
136
- }
137
- if (parsed < min || parsed > max) {
138
- throw new Error(`${id}는 ${min}~${max} 사이여야 합니다.`);
139
- }
140
- return parsed;
141
- }
142
- parseOptionalNumber(id, fieldName, options) {
143
- const raw = this.getValue(id);
144
- const trimmed = raw.trim();
145
- if (!trimmed) {
146
- return undefined;
147
- }
148
- const parsed = Number(trimmed);
149
- if (!Number.isFinite(parsed)) {
150
- throw new Error(`${fieldName}은(는) 유효한 숫자여야 합니다.`);
151
- }
152
- if (parsed < options.min) {
153
- throw new Error(`${fieldName}은(는) ${options.min} 이상이어야 합니다.`);
154
- }
155
- if (options.max !== undefined && parsed > options.max) {
156
- throw new Error(`${fieldName}은(는) ${options.max} 이하여야 합니다.`);
157
- }
158
- if (options.integerOnly !== false && !Number.isInteger(parsed)) {
159
- throw new Error(`${fieldName}은(는) 정수여야 합니다.`);
160
- }
161
- return parsed;
162
- }
163
- /**
164
- * Initialize settings module
165
- */
166
- async init() {
167
- if (this.initialized) {
168
- return;
169
- }
170
- this.initialized = true;
171
- await this.loadSettings();
172
- this.initBackendModelBinding();
173
- this.initDelegatedEventHandlers();
174
- }
175
- initDelegatedEventHandlers() {
176
- if (this.delegatedListenersInitialized) {
177
- return;
178
- }
179
- this.delegatedListenersInitialized = true;
180
- document.addEventListener('change', (e) => {
181
- const target = e.target;
182
- if (!(target instanceof HTMLElement)) {
183
- return;
184
- }
185
- const actionElement = target.closest('[data-action]');
186
- const action = actionElement?.dataset.action;
187
- if (!action || !actionElement) {
188
- return;
189
- }
190
- if (action === 'agent-toggle') {
191
- const checkbox = actionElement;
192
- const agentId = checkbox.dataset.agentId || '';
193
- if (agentId) {
194
- void this.toggleAgent(agentId, checkbox.checked);
195
- }
196
- return;
197
- }
198
- if (action === 'agent-backend') {
199
- const select = actionElement;
200
- const agentId = select.dataset.agentId || '';
201
- if (agentId) {
202
- this.onAgentBackendChange(agentId);
203
- }
204
- return;
205
- }
206
- if (action === 'agent-model') {
207
- const select = actionElement;
208
- const agentId = select.dataset.agentId || '';
209
- if (agentId) {
210
- this.onAgentModelChange(agentId);
211
- }
212
- return;
213
- }
214
- if (action === 'cron-toggle') {
215
- const checkbox = actionElement;
216
- const cronId = checkbox.dataset.cronId || '';
217
- if (cronId) {
218
- void this.toggleCronJob(cronId, checkbox.checked);
219
- }
220
- return;
221
- }
222
- });
223
- document.addEventListener('click', (e) => {
224
- const target = e.target;
225
- if (!(target instanceof HTMLElement)) {
226
- return;
227
- }
228
- // Handle agent-save button click
229
- const saveButton = target.closest('[data-action="agent-save"]');
230
- if (saveButton) {
231
- e.preventDefault();
232
- const agentId = saveButton.dataset.agentId || '';
233
- if (agentId) {
234
- void this.saveAgentConfig(agentId);
235
- }
236
- return;
237
- }
238
- // Handle cron-delete button click
239
- const deleteButton = target.closest('[data-action="cron-delete"]');
240
- if (deleteButton) {
241
- e.preventDefault();
242
- const cronId = deleteButton.dataset.cronId || '';
243
- if (cronId) {
244
- void this.deleteCronJob(cronId);
245
- }
246
- return;
247
- }
248
- const goAgentsButton = target.closest('[data-action="go-agents-tab"]');
249
- if (goAgentsButton) {
250
- e.preventDefault();
251
- if (typeof window.switchTab === 'function') {
252
- window.switchTab('agents');
253
- }
254
- }
255
- });
256
- }
257
- /**
258
- * Load current settings from API
259
- */
260
- async loadSettings() {
261
- this.setStatus('Loading...');
262
- try {
263
- this.config = await API.get('/api/config');
264
- // Load MCP servers data
265
- try {
266
- this.mcpServersData = await API.get('/api/mcp-servers');
267
- }
268
- catch (e) {
269
- logger.warn('MCP servers data unavailable:', e);
270
- this.mcpServersData = { servers: [] };
271
- }
272
- // Load multi-agent data (F3)
273
- try {
274
- this.multiAgentData = await API.get('/api/multi-agent/agents');
275
- }
276
- catch (e) {
277
- logger.warn('Multi-agent data unavailable:', e);
278
- this.multiAgentData = { agents: [] };
279
- }
280
- this.populateForm();
281
- this.setStatus('');
282
- }
283
- catch (error) {
284
- const message = error instanceof Error ? error.message : String(error);
285
- logger.error('Load error:', message);
286
- this.setStatus(`Error: ${message}`, 'error');
287
- }
288
- }
289
- /**
290
- * Populate form with current config values
291
- */
292
- populateForm() {
293
- if (!this.config) {
294
- return;
295
- }
296
- // Discord
297
- this.setCheckbox('settings-discord-enabled', this.config.discord?.enabled);
298
- this.setValue('settings-discord-token', this.config.discord?.token || '', true);
299
- this.setValue('settings-discord-channel', this.config.discord?.default_channel_id || '');
300
- // Slack
301
- this.setCheckbox('settings-slack-enabled', this.config.slack?.enabled);
302
- this.setValue('settings-slack-bot-token', this.config.slack?.bot_token || '', true);
303
- this.setValue('settings-slack-app-token', this.config.slack?.app_token || '', true);
304
- // Telegram
305
- this.setCheckbox('settings-telegram-enabled', this.config.telegram?.enabled);
306
- this.setValue('settings-telegram-token', this.config.telegram?.token || '', true);
307
- // Chatwork
308
- this.setCheckbox('settings-chatwork-enabled', this.config.chatwork?.enabled);
309
- this.setValue('settings-chatwork-token', this.config.chatwork?.api_token || '', true);
310
- // Heartbeat
311
- this.setCheckbox('settings-heartbeat-enabled', this.config.heartbeat?.enabled);
312
- this.setValue('settings-heartbeat-interval', Math.round((this.config.heartbeat?.interval || 1800000) / 60000));
313
- this.setValue('settings-heartbeat-quiet-start', this.config.heartbeat?.quiet_start ?? 23);
314
- this.setValue('settings-heartbeat-quiet-end', this.config.heartbeat?.quiet_end ?? 8);
315
- // Agent
316
- const backend = (this.config.agent?.backend || 'claude');
317
- const model = this.config.agent?.model || 'claude-sonnet-4-6';
318
- const effort = (this.config.agent?.effort || 'medium');
319
- this.setSelectValue('settings-agent-backend', backend);
320
- this.updateModelOptions(backend, model);
321
- const normalizedModel = this.getNormalizedModelForBackend(backend, model);
322
- this.setSelectValue('settings-agent-model', normalizedModel);
323
- this.setSelectValue('settings-agent-effort', effort);
324
- this.updateEffortVisibility(normalizedModel);
325
- this.setValue('settings-agent-max-turns', this.config.agent?.max_turns || 10);
326
- this.setValue('settings-agent-timeout', Math.round((this.config.agent?.timeout || 300000) / 1000));
327
- // Timeouts
328
- this.populateTimeouts();
329
- // Tool Mode
330
- this.populateToolMode();
331
- // Role Permissions
332
- this.populateRoles();
333
- // Multi-Agent Team (F3)
334
- this.populateMultiAgentSection();
335
- // Metrics
336
- this.populateMetricsSection();
337
- // Skills + Token Budget + Cron
338
- this.populateSkillsSection();
339
- this.populateTokenSection();
340
- this.populateCronSection();
341
- reportPageContext('settings', {
342
- pageType: 'settings-overview',
343
- gateways: {
344
- discord: this.config.discord?.enabled ?? false,
345
- slack: this.config.slack?.enabled ?? false,
346
- telegram: this.config.telegram?.enabled ?? false,
347
- chatwork: this.config.chatwork?.enabled ?? false,
348
- },
349
- agent: {
350
- backend: this.config.agent?.backend ?? 'claude',
351
- model: this.config.agent?.model ?? null,
352
- max_turns: this.config.agent?.max_turns ?? null,
353
- timeout: this.config.agent?.timeout ?? null,
354
- },
355
- multiAgentCount: this.multiAgentData.agents.length,
356
- mcpServerCount: this.mcpServersData.servers.length,
357
- });
358
- }
359
- /**
360
- * Populate timeouts section from config
361
- */
362
- populateTimeouts() {
363
- if (!this.config) {
364
- return;
365
- }
366
- const t = this.config.timeouts;
367
- if (!t) {
368
- return;
369
- }
370
- this.setValue('settings-timeout-request', Math.round((t.request_ms ?? 120000) / 1000));
371
- this.setValue('settings-timeout-agent', Math.round((t.agent_ms ?? 300000) / 1000));
372
- this.setValue('settings-timeout-session', Math.round((t.session_ms ?? 1800000) / 60000));
373
- this.setValue('settings-timeout-wf-step', Math.round((t.workflow_step_ms ?? 300000) / 1000));
374
- this.setValue('settings-timeout-wf-max', Math.round((t.workflow_max_ms ?? 1800000) / 60000));
375
- this.setValue('settings-timeout-ultrawork', Math.round((t.ultrawork_ms ?? 300000) / 1000));
376
- }
377
- /**
378
- * Populate role permissions from config
379
- */
380
- populateRoles() {
381
- const container = getElementByIdOrNull('settings-roles-container');
382
- if (!container || !this.config.roles) {
383
- return;
384
- }
385
- const { definitions, sourceMapping } = this.config.roles;
386
- if (!definitions || !sourceMapping) {
387
- return;
388
- }
389
- // Build reverse mapping: role -> sources
390
- const roleSources = {};
391
- for (const [source, role] of Object.entries(sourceMapping)) {
392
- if (!roleSources[role]) {
393
- roleSources[role] = [];
394
- }
395
- roleSources[role].push(source);
396
- }
397
- // Render each role
398
- const roleColors = {
399
- os_agent: { badge: 'green', label: 'Full Access' },
400
- chat_bot: { badge: 'yellow', label: 'Limited' },
401
- };
402
- const roleIcons = {
403
- os_agent: '🖥️',
404
- chat_bot: '🤖',
405
- };
406
- const roleDefs = definitions;
407
- const html = Object.entries(roleDefs)
408
- .map(([roleName, roleConfig]) => {
409
- const sources = roleSources[roleName] || [];
410
- const color = roleColors[roleName] || { badge: 'gray', label: 'Custom' };
411
- const icon = roleIcons[roleName] || '⚙️';
412
- const displayName = escapeHtml(roleName.replace(/_/g, ' ').replace(/\b\w/g, (l) => l.toUpperCase()));
413
- const allowedTools = roleConfig.allowedTools || [];
414
- const blockedTools = roleConfig.blockedTools || [];
415
- const hasSystemControl = roleConfig.systemControl;
416
- const hasSensitiveAccess = roleConfig.sensitiveAccess;
417
- const model = roleConfig.model || 'default';
418
- const maxTurns = roleConfig.maxTurns;
419
- // Format model name for display (and escape)
420
- const displayModel = escapeHtml(formatModelName(model));
421
- return `
422
- <div class="bg-white border border-gray-200 rounded-lg p-2.5">
423
- <div class="flex items-center justify-between mb-2">
424
- <div class="flex items-center gap-2">
425
- <span class="text-xl">${icon}</span>
426
- <h3 class="font-semibold text-gray-900 text-sm">${displayName}</h3>
427
- <span class="text-[10px] bg-${color.badge}-100 text-${color.badge}-800 px-1.5 py-0.5 rounded">${color.label}</span>
428
- </div>
429
- </div>
430
- <div class="text-xs text-gray-600 space-y-1">
431
- <div class="flex items-center gap-2">
432
- <span class="font-medium">Model:</span>
433
- <span class="bg-indigo-50 text-indigo-700 px-1.5 py-0.5 rounded text-[10px] font-medium">${displayModel}</span>
434
- ${maxTurns
435
- ? `<span class="text-gray-400">| ${escapeHtml(String(maxTurns))} turns</span>`
436
- : ''}
437
- </div>
438
- <div><span class="font-medium">Source:</span> ${sources.map((s) => `<code class="bg-gray-100 px-1 rounded">${escapeHtml(s)}</code>`).join(' ')}</div>
439
- <div><span class="font-medium">Allowed:</span> <code class="text-green-600 text-[10px]">${escapeHtml(allowedTools.join(', '))}</code></div>
440
- ${blockedTools.length > 0 ? `<div><span class="font-medium">Blocked:</span> <code class="text-red-600 text-[10px]">${escapeHtml(blockedTools.join(', '))}</code></div>` : ''}
441
- ${hasSystemControl || hasSensitiveAccess
442
- ? `<div><span class="font-medium">Permissions:</span>
443
- ${hasSystemControl ? '<span class="inline-block bg-blue-100 text-blue-800 text-[10px] px-1 rounded mr-1">systemControl</span>' : ''}
444
- ${hasSensitiveAccess ? '<span class="inline-block bg-purple-100 text-purple-800 text-[10px] px-1 rounded">sensitiveAccess</span>' : ''}
445
- </div>`
446
- : ''}
447
- </div>
448
- </div>
449
- `;
450
- })
451
- .join('');
452
- container.innerHTML = html;
453
- }
454
- /**
455
- * Populate tool selection checkboxes
456
- */
457
- populateToolMode() {
458
- const tools = this.config.agent?.tools || { gateway: ['*'], mcp: [] };
459
- const gatewayTools = tools.gateway || ['*'];
460
- const mcpTools = tools.mcp || [];
461
- // Set Gateway tool checkboxes
462
- const gatewayCheckboxes = document.querySelectorAll('.gateway-tool');
463
- const isGatewayAll = gatewayTools.includes('*');
464
- gatewayCheckboxes.forEach((cb) => {
465
- if (isGatewayAll) {
466
- cb.checked = true;
467
- }
468
- else {
469
- cb.checked = gatewayTools.includes(cb.value);
470
- }
471
- });
472
- // Set Select All checkbox
473
- const gatewaySelectAll = getElementByIdOrNull('gateway-select-all');
474
- if (gatewaySelectAll) {
475
- gatewaySelectAll.checked = isGatewayAll || this.allChecked('.gateway-tool');
476
- }
477
- // Dynamically render MCP servers from API
478
- this.renderMCPServers(mcpTools);
479
- // Update summary
480
- this.updateToolSummary();
481
- }
482
- /**
483
- * Render MCP servers dynamically from loaded data
484
- */
485
- renderMCPServers(selectedTools = []) {
486
- const container = getElementByIdOrNull('mcp-tools-list');
487
- if (!container) {
488
- return;
489
- }
490
- const servers = (this.mcpServersData?.servers || []);
491
- const isMCPAll = selectedTools.includes('*');
492
- if (servers.length === 0) {
493
- container.innerHTML = `
494
- <p class="text-xs text-gray-500 col-span-full">
495
- No MCP servers configured. Add servers to ~/.mama/mama-mcp-config.json
496
- </p>
497
- `;
498
- return;
499
- }
500
- const serverColors = {
501
- 'brave-devtools': { border: 'border-blue-200', bg: 'bg-blue-50', icon: '🌐' },
502
- 'brave-search': { border: 'border-orange-200', bg: 'bg-orange-50', icon: '🔍' },
503
- mama: { border: 'border-purple-200', bg: 'bg-purple-50', icon: '🧠' },
504
- default: { border: 'border-gray-200', bg: 'bg-gray-50', icon: '🔌' },
505
- };
506
- const html = servers
507
- .map((server) => {
508
- const serverName = server.name || '';
509
- const colors = serverColors[serverName] || serverColors['default'];
510
- const toolValue = `mcp__${serverName}__*`;
511
- const isChecked = isMCPAll || selectedTools.includes(toolValue);
512
- // Escape server.name for safe HTML rendering (XSS prevention)
513
- const safeName = escapeHtml(serverName);
514
- const safeToolValue = escapeAttr(toolValue);
515
- return `
516
- <label class="flex items-center gap-2 p-2 border ${colors.border} rounded-lg text-xs cursor-pointer hover:${colors.bg}">
517
- <input type="checkbox" class="mcp-tool" value="${safeToolValue}" ${isChecked ? 'checked' : ''}>
518
- ${colors.icon} ${safeName}
519
- </label>
520
- `;
521
- })
522
- .join('');
523
- container.innerHTML = html;
524
- // Update Select All checkbox
525
- const mcpSelectAll = getElementByIdOrNull('mcp-select-all');
526
- if (mcpSelectAll) {
527
- mcpSelectAll.checked = isMCPAll || this.allChecked('.mcp-tool');
528
- }
529
- }
530
- /**
531
- * Check if all checkboxes of a class are checked
532
- */
533
- allChecked(selector) {
534
- const checkboxes = document.querySelectorAll(selector);
535
- return Array.from(checkboxes).every((cb) => cb.checked);
536
- }
537
- /**
538
- * Toggle all Gateway tools
539
- */
540
- toggleAllGateway(checked) {
541
- document.querySelectorAll('.gateway-tool').forEach((cb) => {
542
- cb.checked = checked;
543
- });
544
- this.updateToolSummary();
545
- }
546
- /**
547
- * Toggle all MCP tools
548
- */
549
- toggleAllMCP(checked) {
550
- document.querySelectorAll('.mcp-tool').forEach((cb) => {
551
- cb.checked = checked;
552
- });
553
- this.updateToolSummary();
554
- }
555
- /**
556
- * Update tool summary display
557
- */
558
- updateToolSummary() {
559
- const gatewayCount = document.querySelectorAll('.gateway-tool:checked').length;
560
- const mcpCount = document.querySelectorAll('.mcp-tool:checked').length;
561
- const summaryEl = getElementByIdOrNull('tool-summary');
562
- if (summaryEl) {
563
- summaryEl.textContent = `Gateway: ${gatewayCount} tools | MCP: ${mcpCount} tools`;
564
- }
565
- }
566
- /**
567
- * Save settings and restart daemon to apply changes
568
- */
569
- async saveAndRestart() {
570
- this.setStatus('Saving...');
571
- try {
572
- const updates = this.collectFormData();
573
- await API.put('/api/config', updates);
574
- this.setStatus('Saved! Restarting...', 'success');
575
- showToast('Settings saved. Restarting daemon...');
576
- // Trigger restart after save
577
- try {
578
- await API.post('/api/restart', {});
579
- }
580
- catch {
581
- // Expected: connection drops when server exits
582
- }
583
- const isServiceReady = await this.waitForServiceAfterRestart();
584
- if (!isServiceReady) {
585
- this.setStatus('Restarted, but reconnect timed out. Please refresh manually.', 'error');
586
- showToast('Restart request sent. Auto reconnect timed out - please refresh page.');
587
- return;
588
- }
589
- this.setStatus('Reconnected. Reloading page...', 'success');
590
- setTimeout(() => {
591
- window.location.reload();
592
- }, 400);
593
- }
594
- catch (error) {
595
- const message = error instanceof Error ? error.message : String(error);
596
- logger.error('Save error:', message);
597
- this.setStatus(`Error: ${message}`, 'error');
598
- showToast(`Failed to save: ${message}`);
599
- }
600
- }
601
- /**
602
- * Wait for service recovery after restart by polling dashboard status endpoint.
603
- */
604
- async waitForServiceAfterRestart() {
605
- const maxAttempts = 40;
606
- const intervalMs = 1000;
607
- const readinessChecks = ['/api/health', '/api/dashboard/status'];
608
- // Server waits 500ms + shell sleeps 1s before stopping, so wait at least 2.5s
609
- // before first poll to ensure old server is actually down
610
- const initialDelayMs = 2500;
611
- this.setStatus('Restarting... waiting for shutdown', '');
612
- await new Promise((resolve) => setTimeout(resolve, initialDelayMs));
613
- for (let attempt = 1; attempt <= maxAttempts; attempt++) {
614
- this.setStatus(`Restarting... reconnecting (${attempt}/${maxAttempts})`, '');
615
- let isReady = false;
616
- for (const endpoint of readinessChecks) {
617
- try {
618
- // Use shared API client for strict JSON response parsing and consistent errors.
619
- await API.get(endpoint);
620
- logger.debug('[Settings] Service ready check passed:', endpoint);
621
- isReady = true;
622
- break;
623
- }
624
- catch {
625
- logger.debug('[Settings] Service not ready yet:', endpoint);
626
- }
627
- }
628
- if (isReady) {
629
- return true;
630
- }
631
- await new Promise((resolve) => setTimeout(resolve, intervalMs));
632
- }
633
- return false;
634
- }
635
- /**
636
- * Collect form data into config update object
637
- */
638
- collectFormData() {
639
- const backend = (this.getSelectValue('settings-agent-backend') ||
640
- this.config?.agent?.backend ||
641
- 'claude');
642
- const model = this.getSelectValue('settings-agent-model') || this.config?.agent?.model || '';
643
- const effort = (this.getSelectValue('settings-agent-effort') ||
644
- this.config?.agent?.effort ||
645
- 'medium');
646
- const useClaudeCli = backend === 'claude';
647
- const resolvedModel = this.getNormalizedModelForBackend(backend, model);
648
- const normalizedEffort = this.supportsEffortModel(resolvedModel)
649
- ? this.normalizeEffortForModel(resolvedModel, effort)
650
- : undefined;
651
- const hasToolControls = document.querySelectorAll('.gateway-tool, .mcp-tool').length > 0;
652
- const configuredTools = this.config?.agent?.tools;
653
- const tools = hasToolControls
654
- ? this.collectToolModeData()
655
- : {
656
- gateway: configuredTools?.gateway ?? [],
657
- mcp: configuredTools?.mcp ?? [],
658
- mcp_config: typeof configuredTools?.mcp_config === 'string'
659
- ? configuredTools.mcp_config
660
- : '~/.mama/mama-mcp-config.json',
661
- };
662
- // Get token values - if empty and original was masked, keep original
663
- const discordToken = this.getTokenValue('settings-discord-token', this.config?.discord?.token);
664
- const slackBotToken = this.getTokenValue('settings-slack-bot-token', this.config?.slack?.bot_token);
665
- const slackAppToken = this.getTokenValue('settings-slack-app-token', this.config?.slack?.app_token);
666
- const telegramToken = this.getTokenValue('settings-telegram-token', this.config?.telegram?.token);
667
- const chatworkToken = this.getTokenValue('settings-chatwork-token', this.config?.chatwork?.api_token);
668
- return {
669
- discord: {
670
- enabled: this.getCheckbox('settings-discord-enabled'),
671
- token: discordToken,
672
- default_channel_id: this.getValue('settings-discord-channel'),
673
- },
674
- slack: {
675
- enabled: this.getCheckbox('settings-slack-enabled'),
676
- bot_token: slackBotToken,
677
- app_token: slackAppToken,
678
- },
679
- telegram: {
680
- enabled: this.getCheckbox('settings-telegram-enabled'),
681
- token: telegramToken,
682
- },
683
- chatwork: {
684
- enabled: this.getCheckbox('settings-chatwork-enabled'),
685
- api_token: chatworkToken,
686
- },
687
- heartbeat: {
688
- enabled: this.getCheckbox('settings-heartbeat-enabled'),
689
- interval: this.parseIntegerInput('settings-heartbeat-interval', 1, 1440, 30) * 60000,
690
- quiet_start: this.parseIntegerInput('settings-heartbeat-quiet-start', 0, 23, 23),
691
- quiet_end: this.parseIntegerInput('settings-heartbeat-quiet-end', 0, 23, 8),
692
- },
693
- use_claude_cli: useClaudeCli,
694
- agent: {
695
- backend,
696
- model: resolvedModel,
697
- // Effort for Claude 4.6 models (adaptive thinking). 'max' is Opus-only.
698
- effort: normalizedEffort,
699
- max_turns: this.parseIntegerInput('settings-agent-max-turns', 1, 100, this.config?.agent?.max_turns ?? 10),
700
- timeout: this.parseIntegerInput('settings-agent-timeout', 1, 600, Math.round((this.config?.agent?.timeout ?? 300000) / 1000)) * 1000,
701
- tools,
702
- },
703
- token_budget: {
704
- // Keep existing integer constraint for daily limit to avoid partial values.
705
- daily_limit: this.parseOptionalNumber('settings-token-daily-limit', 'daily_limit', {
706
- min: 0,
707
- integerOnly: true,
708
- }),
709
- alert_threshold: this.parseOptionalNumber('settings-token-alert-threshold', 'alert_threshold', {
710
- min: 0,
711
- max: 100,
712
- integerOnly: false,
713
- }),
714
- },
715
- metrics: {
716
- enabled: this.getCheckbox('settings-metrics-enabled'),
717
- retention_days: this.parseIntegerInput('settings-metrics-retention', 1, 90, 7),
718
- },
719
- timeouts: {
720
- request_ms: this.parseIntegerInput('settings-timeout-request', 0, 600, 120) * 1000,
721
- agent_ms: this.parseIntegerInput('settings-timeout-agent', 0, 3600, 300) * 1000,
722
- session_ms: this.parseIntegerInput('settings-timeout-session', 0, 1440, 30) * 60000,
723
- workflow_step_ms: this.parseIntegerInput('settings-timeout-wf-step', 0, 7200, 300) * 1000,
724
- workflow_max_ms: this.parseIntegerInput('settings-timeout-wf-max', 0, 1440, 30) * 60000,
725
- ultrawork_ms: this.parseIntegerInput('settings-timeout-ultrawork', 0, 7200, 300) * 1000,
726
- },
727
- };
728
- }
729
- initBackendModelBinding() {
730
- if (this.backendListenersInitialized) {
731
- return;
732
- }
733
- this.backendListenersInitialized = true;
734
- const backendSelect = getElementByIdOrNull('settings-agent-backend');
735
- if (!backendSelect) {
736
- return;
737
- }
738
- backendSelect.addEventListener('change', () => {
739
- const backend = (this.getSelectValue('settings-agent-backend') || 'claude');
740
- const currentModel = this.getSelectValue('settings-agent-model');
741
- this.updateModelOptions(backend, currentModel);
742
- const normalizedModel = this.getNormalizedModelForBackend(backend, currentModel);
743
- this.setSelectValue('settings-agent-model', normalizedModel);
744
- this.updateEffortVisibility(normalizedModel);
745
- });
746
- // Also listen for model changes to update effort visibility
747
- const modelSelect = getElementByIdOrNull('settings-agent-model');
748
- if (modelSelect) {
749
- modelSelect.addEventListener('change', () => {
750
- const model = this.getSelectValue('settings-agent-model');
751
- this.updateEffortVisibility(model);
752
- });
753
- }
754
- }
755
- updateModelOptions(backend, currentModel) {
756
- const select = getElementByIdOrNull('settings-agent-model');
757
- if (!select) {
758
- return;
759
- }
760
- const normalized = this.getNormalizedModelForBackend(backend, currentModel);
761
- const modelList = getSettingsModelOptions(backend, normalized);
762
- select.innerHTML = modelList
763
- .map((m) => `<option value="${escapeHtml(m)}" ${m === normalized ? 'selected' : ''}>${escapeHtml(formatModelName(m))}</option>`)
764
- .join('');
765
- // Update effort visibility when model options change
766
- this.updateEffortVisibility(normalized);
767
- }
768
- /**
769
- * Show/hide effort level dropdown based on model selection
770
- * Effort applies to Claude 4.6 models, with 'max' reserved for Opus.
771
- */
772
- updateEffortVisibility(model) {
773
- const effortContainer = getElementByIdOrNull('settings-effort-container');
774
- if (!effortContainer) {
775
- return;
776
- }
777
- const supportsEffort = this.supportsEffortModel(model);
778
- effortContainer.style.display = supportsEffort ? 'block' : 'none';
779
- const effortSelect = getElementByIdOrNull('settings-agent-effort');
780
- if (!effortSelect) {
781
- return;
782
- }
783
- const currentEffort = (effortSelect.value || 'medium');
784
- const normalizedEffort = this.normalizeEffortForModel(model, currentEffort);
785
- effortSelect.innerHTML = this.buildEffortOptions(model, normalizedEffort);
786
- effortSelect.value = normalizedEffort;
787
- }
788
- getNormalizedModelForBackend(backend, model) {
789
- const isCodexBackend = backend === 'codex';
790
- if (!model) {
791
- return DEFAULT_MODEL[backend];
792
- }
793
- const isClaudeModel = /^claude-/i.test(model);
794
- if (isCodexBackend && isClaudeModel) {
795
- return DEFAULT_MODEL.codex;
796
- }
797
- if (backend === 'cline' && (isClaudeModel || /^gpt-/i.test(model))) {
798
- return DEFAULT_MODEL.cline;
799
- }
800
- if (backend === 'claude' && !isClaudeModel) {
801
- return DEFAULT_MODEL.claude;
802
- }
803
- return model;
804
- }
805
- /**
806
- * Collect tool selection data from checkboxes
807
- */
808
- collectToolModeData() {
809
- const gatewayTools = [];
810
- const mcpTools = [];
811
- // Collect selected Gateway tools
812
- document.querySelectorAll('.gateway-tool:checked').forEach((cb) => {
813
- gatewayTools.push(cb.value);
814
- });
815
- // Collect selected MCP tools
816
- document.querySelectorAll('.mcp-tool:checked').forEach((cb) => {
817
- mcpTools.push(cb.value);
818
- });
819
- // If all Gateway tools are selected, use wildcard
820
- const allGateway = document.querySelectorAll('.gateway-tool');
821
- if (gatewayTools.length === allGateway.length && gatewayTools.length > 0) {
822
- return {
823
- gateway: ['*'],
824
- mcp: mcpTools,
825
- mcp_config: '~/.mama/mama-mcp-config.json',
826
- };
827
- }
828
- return {
829
- gateway: gatewayTools,
830
- mcp: mcpTools,
831
- mcp_config: '~/.mama/mama-mcp-config.json',
832
- };
833
- }
834
- /**
835
- * Reset form to current saved values
836
- */
837
- resetForm() {
838
- this.populateForm();
839
- this.setStatus('Form reset');
840
- setTimeout(() => this.setStatus(''), 2000);
841
- }
842
- /**
843
- * Helper: Set checkbox value
844
- */
845
- setCheckbox(id, checked) {
846
- const el = getElementByIdOrNull(id);
847
- if (el) {
848
- el.checked = !!checked;
849
- }
850
- }
851
- /**
852
- * Helper: Get checkbox value
853
- */
854
- getCheckbox(id) {
855
- const el = getElementByIdOrNull(id);
856
- return el ? el.checked : false;
857
- }
858
- /**
859
- * Helper: Set input value
860
- * @param {string} id - Element ID
861
- * @param {string} value - Value to set
862
- * @param {boolean} isSensitive - If true, treat as sensitive token (keep if masked)
863
- */
864
- setValue(id, value, isSensitive = false) {
865
- const el = getElementByIdOrNull(id);
866
- if (el) {
867
- // For sensitive fields (tokens), preserve placeholder if value is masked
868
- const normalized = String(value ?? '');
869
- if (isSensitive && this.isMaskedToken(normalized)) {
870
- el.placeholder = normalized;
871
- el.value = '';
872
- }
873
- else {
874
- el.value = normalized;
875
- }
876
- }
877
- }
878
- /**
879
- * Check if a token is masked (e.g., "***[redacted]***")
880
- */
881
- isMaskedToken(token) {
882
- if (token === undefined || token === null) {
883
- return false;
884
- }
885
- const str = String(token);
886
- return str === '***[redacted]***' || (str.startsWith('***[') && str.endsWith(']***'));
887
- }
888
- /**
889
- * Get token value from input, preserving original if input is empty and original was masked
890
- * @param {string} id - Input element ID
891
- * @param {string} originalToken - Original token value from config
892
- * @returns {string} Token to send (either new value or original masked token)
893
- */
894
- getTokenValue(id, originalToken) {
895
- const inputValue = this.getValue(id);
896
- // If user entered a new value, use it
897
- if (inputValue && inputValue.trim() !== '') {
898
- return inputValue;
899
- }
900
- // If input is empty and original was masked, keep the masked token (backend will preserve it)
901
- if (this.isMaskedToken(originalToken)) {
902
- return originalToken;
903
- }
904
- // Otherwise return the input value (may be empty)
905
- return inputValue;
906
- }
907
- /**
908
- * Helper: Get input value
909
- */
910
- getValue(id) {
911
- const el = getElementByIdOrNull(id);
912
- return el ? el.value : '';
913
- }
914
- /**
915
- * Helper: Set select value
916
- */
917
- setSelectValue(id, value) {
918
- const el = getElementByIdOrNull(id);
919
- if (el) {
920
- el.value = value;
921
- }
922
- }
923
- /**
924
- * Helper: Get select value
925
- */
926
- getSelectValue(id) {
927
- const el = getElementByIdOrNull(id);
928
- return el ? el.value : '';
929
- }
930
- /**
931
- * Helper: Set radio button
932
- */
933
- setRadio(id, checked) {
934
- const el = getElementByIdOrNull(id);
935
- if (el) {
936
- el.checked = !!checked;
937
- }
938
- }
939
- /**
940
- * Helper: Get radio button value
941
- */
942
- getRadio(id) {
943
- const el = getElementByIdOrNull(id);
944
- return el ? el.checked : false;
945
- }
946
- /**
947
- * Set status message
948
- */
949
- setStatus(message, type = '') {
950
- const statusEl = getElementByIdOrNull('settings-status');
951
- if (statusEl) {
952
- statusEl.textContent = message;
953
- statusEl.className = `text-sm ${type === 'error' ? 'text-red-500' : type === 'success' ? 'text-green-500' : 'text-gray-500'}`;
954
- }
955
- }
956
- /**
957
- * Populate Multi-Agent Team section (F3)
958
- * Agent editing has moved to the Agents tab — show redirect notice.
959
- */
960
- populateMultiAgentSection() {
961
- const container = getElementByIdOrNull('settings-multi-agent-container');
962
- if (!container) {
963
- return;
964
- }
965
- container.innerHTML = `
966
- <div class="bg-white border border-gray-200 rounded-lg p-4 text-center">
967
- <p class="text-sm text-gray-600 mb-2">Agent management has moved to the Agents tab.</p>
968
- <button data-action="go-agents-tab"
969
- class="text-sm px-4 py-1.5 rounded-md bg-mama-yellow text-mama-black hover:bg-mama-yellow-hover font-medium">
970
- Go to Agents
971
- </button>
972
- </div>`;
973
- }
974
- /**
975
- * Toggle agent enabled status (F3)
976
- */
977
- async toggleAgent(agentId, enabled) {
978
- try {
979
- await API.put(`/api/multi-agent/agents/${agentId}`, { enabled });
980
- logger.info(`Agent ${agentId} ${enabled ? 'enabled' : 'disabled'}`);
981
- }
982
- catch (error) {
983
- logger.error('Failed to toggle agent:', error);
984
- // Revert checkbox on error
985
- const checkbox = document.querySelector(`input[data-action="agent-toggle"][data-agent-id="${agentId}"]`);
986
- if (checkbox) {
987
- checkbox.checked = !enabled;
988
- }
989
- alert(`Failed to update agent: ${error instanceof Error ? error.message : String(error)}`);
990
- }
991
- }
992
- onAgentBackendChange(agentId) {
993
- const backendSelect = getElementByIdOrNull(`agent-backend-${agentId}`);
994
- const modelSelect = getElementByIdOrNull(`agent-model-${agentId}`);
995
- if (!backendSelect || !modelSelect) {
996
- return;
997
- }
998
- const backend = (backendSelect.value || 'claude');
999
- const currentModel = modelSelect.value || '';
1000
- const normalized = this.getNormalizedModelForBackend(backend, currentModel);
1001
- const options = getSettingsModelOptions(backend, normalized);
1002
- modelSelect.innerHTML = options
1003
- .map((m) => `<option value="${escapeAttr(m)}" ${m === normalized ? 'selected' : ''}>${escapeHtml(formatModelName(m))}</option>`)
1004
- .join('');
1005
- this.refreshAgentEffortControls(agentId, normalized);
1006
- }
1007
- onAgentModelChange(agentId) {
1008
- const modelSelect = getElementByIdOrNull(`agent-model-${agentId}`);
1009
- if (!modelSelect) {
1010
- return;
1011
- }
1012
- const model = modelSelect.value || '';
1013
- this.refreshAgentEffortControls(agentId, model);
1014
- }
1015
- async saveAgentConfig(agentId) {
1016
- try {
1017
- const backendSelect = getElementByIdOrNull(`agent-backend-${agentId}`);
1018
- const modelSelect = getElementByIdOrNull(`agent-model-${agentId}`);
1019
- const delegateCheckbox = getElementByIdOrNull(`agent-delegate-${agentId}`);
1020
- const allToolsCheckbox = getElementByIdOrNull(`agent-alltools-${agentId}`);
1021
- if (!backendSelect || !modelSelect) {
1022
- throw new Error('Agent settings inputs not found');
1023
- }
1024
- const backend = (backendSelect.value || 'claude');
1025
- const model = this.getNormalizedModelForBackend(backend, modelSelect.value || '');
1026
- const can_delegate = delegateCheckbox?.checked ?? false;
1027
- const hasAllTools = allToolsCheckbox?.checked ?? false;
1028
- // Effort level
1029
- const effortSelect = getElementByIdOrNull(`agent-effort-${agentId}`);
1030
- const supportsEffort = this.supportsEffortModel(model);
1031
- const effort = supportsEffort && effortSelect
1032
- ? this.normalizeEffortForModel(model, effortSelect.value)
1033
- : undefined;
1034
- // Build tool_permissions based on checkbox
1035
- const tool_permissions = hasAllTools
1036
- ? { allowed: ['*'], blocked: [] }
1037
- : { allowed: ['Read', 'Grep', 'Glob'], blocked: [] };
1038
- await API.put(`/api/multi-agent/agents/${agentId}`, {
1039
- backend,
1040
- model,
1041
- effort,
1042
- can_delegate,
1043
- tool_permissions,
1044
- });
1045
- showToast(`Saved ${agentId} (applied)`);
1046
- await this.loadSettings();
1047
- }
1048
- catch (error) {
1049
- logger.error('Failed to save agent config:', error);
1050
- alert(`Failed to save agent config: ${error instanceof Error ? error.message : String(error)}`);
1051
- }
1052
- }
1053
- /**
1054
- * Populate installed skills section
1055
- */
1056
- async populateSkillsSection() {
1057
- const container = getElementByIdOrNull('settings-skills-container');
1058
- if (!container) {
1059
- return;
1060
- }
1061
- try {
1062
- const { skills } = await API.get('/api/skills');
1063
- if (!skills || skills.length === 0) {
1064
- container.innerHTML = '<p class="text-xs text-gray-400">No skills installed</p>';
1065
- return;
1066
- }
1067
- const sourceColors = {
1068
- mama: 'bg-yellow-100 text-yellow-700',
1069
- cowork: 'bg-blue-100 text-blue-700',
1070
- external: 'bg-purple-100 text-purple-700',
1071
- };
1072
- container.innerHTML = `
1073
- <div class="space-y-1.5">
1074
- ${skills
1075
- .map((s) => `
1076
- <div class="flex items-center justify-between py-1">
1077
- <div class="flex items-center gap-2">
1078
- <span class="text-xs font-medium text-gray-900">${escapeHtml(s.name)}</span>
1079
- <span class="text-[10px] px-1.5 py-0.5 rounded ${sourceColors[s.source] || 'bg-gray-100 text-gray-600'}">${escapeHtml(s.source)}</span>
1080
- </div>
1081
- <label class="relative inline-flex items-center cursor-pointer">
1082
- <input type="checkbox" ${s.enabled !== false ? 'checked' : ''}
1083
- data-skill-source="${escapeHtml(s.source)}"
1084
- data-skill-id="${escapeHtml(s.id)}"
1085
- class="sr-only peer">
1086
- <div class="w-9 h-5 bg-gray-200 peer-focus:ring-2 peer-focus:ring-yellow-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-yellow-400"></div>
1087
- </label>
1088
- </div>
1089
- `)
1090
- .join('')}
1091
- </div>
1092
- `;
1093
- container.querySelectorAll('input[data-skill-id]').forEach((input) => {
1094
- input.addEventListener('change', (event) => {
1095
- const target = event.target;
1096
- if (!(target instanceof HTMLInputElement)) {
1097
- return;
1098
- }
1099
- const source = target.dataset.skillSource || '';
1100
- const id = target.dataset.skillId || '';
1101
- if (!source || !id) {
1102
- return;
1103
- }
1104
- this.toggleSkill(source, id, target.checked);
1105
- });
1106
- });
1107
- }
1108
- catch (error) {
1109
- logger.warn('Skills load error:', error instanceof Error ? error.message : String(error));
1110
- container.innerHTML = '<p class="text-xs text-gray-400">Failed to load skills</p>';
1111
- }
1112
- }
1113
- /**
1114
- * Toggle skill enabled/disabled from settings
1115
- */
1116
- async toggleSkill(source, name, enabled) {
1117
- try {
1118
- await API.toggleSkill(name, enabled, source);
1119
- }
1120
- catch (error) {
1121
- logger.error('Skill toggle failed:', error instanceof Error ? error.message : String(error));
1122
- this.populateSkillsSection();
1123
- }
1124
- }
1125
- /**
1126
- * Populate scheduled jobs section
1127
- */
1128
- async populateCronSection() {
1129
- const container = getElementByIdOrNull('settings-cron-container');
1130
- if (!container) {
1131
- return;
1132
- }
1133
- try {
1134
- const { jobs } = await API.get('/api/cron');
1135
- if (!jobs || jobs.length === 0) {
1136
- container.innerHTML = '<p class="text-xs text-gray-400">No scheduled jobs</p>';
1137
- return;
1138
- }
1139
- const cronJobs = jobs;
1140
- container.innerHTML = `<div class="space-y-1.5">${cronJobs
1141
- .map((job) => {
1142
- const nextRun = job.nextRun
1143
- ? new Date(job.nextRun).toLocaleString([], {
1144
- month: 'short',
1145
- day: 'numeric',
1146
- hour: '2-digit',
1147
- minute: '2-digit',
1148
- })
1149
- : '-';
1150
- return `
1151
- <div class="flex items-center justify-between py-1">
1152
- <div class="flex-1 min-w-0">
1153
- <div class="flex items-center gap-2">
1154
- <span class="text-xs font-medium text-gray-900">${escapeHtml(job.name || job.id)}</span>
1155
- <code class="text-[10px] bg-gray-100 px-1 rounded">${escapeHtml(job.schedule || job.cronExpr || '')}</code>
1156
- </div>
1157
- <p class="text-[10px] text-gray-500 truncate">${escapeHtml((job.prompt || '').slice(0, 80))}</p>
1158
- </div>
1159
- <div class="flex items-center gap-1 ml-2 shrink-0">
1160
- <span class="text-[10px] text-gray-400">${nextRun}</span>
1161
- <label class="relative inline-flex items-center cursor-pointer">
1162
- <input
1163
- type="checkbox"
1164
- ${job.enabled !== false ? 'checked' : ''}
1165
- data-action="cron-toggle"
1166
- data-cron-id="${escapeAttr(job.id)}"
1167
- class="sr-only peer cron-toggle"
1168
- >
1169
- <div class="w-9 h-5 bg-gray-200 peer-focus:ring-2 peer-focus:ring-yellow-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-yellow-400"></div>
1170
- </label>
1171
- <button
1172
- type="button"
1173
- data-action="cron-delete"
1174
- data-cron-id="${escapeAttr(job.id)}"
1175
- class="text-red-400 hover:text-red-600 text-xs px-1"
1176
- title="Delete"
1177
- >
1178
- ✕
1179
- </button>
1180
- </div>
1181
- </div>`;
1182
- })
1183
- .join('')}</div>`;
1184
- }
1185
- catch (error) {
1186
- logger.warn('Cron load error:', error);
1187
- container.innerHTML = '<p class="text-xs text-gray-400">Failed to load jobs</p>';
1188
- }
1189
- }
1190
- async addCronJob() {
1191
- const nameInput = getElementByIdOrNull('settings-cron-name');
1192
- const cronExprInput = getElementByIdOrNull('settings-cron-expr');
1193
- const promptInput = getElementByIdOrNull('settings-cron-prompt');
1194
- if (!nameInput || !cronExprInput || !promptInput) {
1195
- return;
1196
- }
1197
- const name = nameInput.value.trim();
1198
- const cronExpr = cronExprInput.value.trim();
1199
- const prompt = promptInput.value.trim();
1200
- if (!name || !cronExpr || !prompt) {
1201
- showToast('Please fill in all fields');
1202
- return;
1203
- }
1204
- try {
1205
- await API.post('/api/cron', { name, cron_expr: cronExpr, prompt });
1206
- nameInput.value = '';
1207
- cronExprInput.value = '';
1208
- promptInput.value = '';
1209
- showToast('Job created');
1210
- this.populateCronSection();
1211
- }
1212
- catch (error) {
1213
- showToast(`Failed: ${error instanceof Error ? error.message : String(error)}`);
1214
- }
1215
- }
1216
- async toggleCronJob(id, enabled) {
1217
- try {
1218
- await API.updateCronJob(id, { enabled });
1219
- }
1220
- catch (error) {
1221
- logger.error('Cron toggle failed:', error);
1222
- this.populateCronSection();
1223
- }
1224
- }
1225
- async deleteCronJob(id) {
1226
- if (!confirm('Delete this scheduled job?')) {
1227
- return;
1228
- }
1229
- try {
1230
- await API.del(`/api/cron/${encodeURIComponent(id)}`);
1231
- showToast('Job deleted');
1232
- this.populateCronSection();
1233
- }
1234
- catch (error) {
1235
- showToast(`Failed: ${error instanceof Error ? error.message : String(error)}`);
1236
- }
1237
- }
1238
- /**
1239
- * Populate metrics section from config
1240
- */
1241
- populateMetricsSection() {
1242
- const metrics = this.config?.metrics;
1243
- this.setCheckbox('settings-metrics-enabled', metrics?.enabled !== false);
1244
- this.setValue('settings-metrics-retention', metrics?.retention_days ?? 7);
1245
- }
1246
- /**
1247
- * Populate token budget section from config
1248
- */
1249
- populateTokenSection() {
1250
- const budget = this.config?.token_budget;
1251
- if (!budget) {
1252
- return;
1253
- }
1254
- this.setValue('settings-token-daily-limit', budget.daily_limit || '');
1255
- this.setValue('settings-token-alert-threshold', budget.alert_threshold || '');
1256
- }
1257
- }