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