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