cli-jaw 0.1.11 → 0.1.12
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.ko.md +44 -13
- package/README.md +12 -11
- package/README.zh-CN.md +43 -12
- package/dist/bin/commands/doctor.js +13 -2
- package/dist/bin/commands/doctor.js.map +1 -1
- package/dist/bin/commands/mcp.js +15 -18
- package/dist/bin/commands/mcp.js.map +1 -1
- package/dist/bin/commands/serve.js +3 -28
- package/dist/bin/commands/serve.js.map +1 -1
- package/dist/bin/commands/skill.js +9 -6
- package/dist/bin/commands/skill.js.map +1 -1
- package/dist/lib/mcp-sync.js +123 -31
- package/dist/lib/mcp-sync.js.map +1 -1
- package/{scripts → dist/scripts}/check-copilot-gap.js +24 -17
- package/dist/scripts/check-copilot-gap.js.map +1 -0
- package/{scripts/check-deps-offline.mjs → dist/scripts/check-deps-offline.js} +24 -20
- package/dist/scripts/check-deps-offline.js.map +1 -0
- package/dist/scripts/fresh-install-smoke.js +120 -0
- package/dist/scripts/fresh-install-smoke.js.map +1 -0
- package/{scripts/i18n-registry.py → dist/scripts/i18n-registry.js} +115 -122
- package/dist/scripts/i18n-registry.js.map +1 -0
- package/dist/server.js +34 -26
- package/dist/server.js.map +1 -1
- package/dist/src/cli/command-context.js +13 -3
- package/dist/src/cli/command-context.js.map +1 -1
- package/dist/src/prompt/builder.js +28 -1
- package/dist/src/prompt/builder.js.map +1 -1
- package/package.json +9 -5
- package/public/dist/bundle.js +72 -77
- package/public/dist/bundle.js.map +4 -4
- package/public/index.html +1 -3
- package/public/js/{api.js → api.ts} +18 -12
- package/public/js/{constants.js → constants.ts} +44 -24
- package/public/js/features/{appname.js → appname.ts} +13 -12
- package/public/js/features/{chat.js → chat.ts} +46 -37
- package/public/js/features/{employees.js → employees.ts} +67 -38
- package/public/js/features/heartbeat.ts +90 -0
- package/public/js/features/{i18n.js → i18n.ts} +20 -20
- package/public/js/features/memory.ts +125 -0
- package/public/js/features/{settings.js → settings.ts} +125 -93
- package/public/js/features/{sidebar.js → sidebar.ts} +15 -16
- package/public/js/features/{skills.js → skills.ts} +29 -16
- package/public/js/features/{slash-commands.js → slash-commands.ts} +34 -29
- package/public/js/features/{theme.js → theme.ts} +4 -4
- package/public/js/{locale.js → locale.ts} +3 -3
- package/public/js/main.ts +280 -0
- package/public/js/{render.js → render.ts} +34 -107
- package/public/js/state.ts +38 -0
- package/public/js/{ui.js → ui.ts} +60 -63
- package/public/js/{ws.js → ws.ts} +46 -20
- package/public/locales/en.json +1 -0
- package/public/locales/ko.json +1 -0
- package/scripts/check-copilot-gap.ts +75 -0
- package/scripts/check-deps-offline.ts +98 -0
- package/scripts/fresh-install-smoke.ts +130 -0
- package/scripts/i18n-registry.ts +230 -0
- package/scripts/postinstall-guard.cjs +5 -0
- package/dist/bin/cli-claw.js +0 -96
- package/dist/bin/cli-claw.js.map +0 -1
- package/public/js/features/heartbeat.js +0 -80
- package/public/js/features/memory.js +0 -85
- package/public/js/main.js +0 -278
- package/public/js/state.js +0 -16
|
@@ -3,23 +3,29 @@ import { getPreferredLocale } from '../locale.js';
|
|
|
3
3
|
import { t } from './i18n.js';
|
|
4
4
|
import { api } from '../api.js';
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
interface SlashCommand {
|
|
7
|
+
name: string;
|
|
8
|
+
desc: string;
|
|
9
|
+
args?: string;
|
|
10
|
+
category?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let cmdList: SlashCommand[] = [];
|
|
14
|
+
let filtered: SlashCommand[] = [];
|
|
15
|
+
let selectedIdx = -1;
|
|
9
16
|
let isOpen = false;
|
|
10
|
-
let closeTimer = null;
|
|
17
|
+
let closeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
11
18
|
|
|
12
|
-
|
|
13
|
-
function looksLikeFilePath(text) {
|
|
19
|
+
function looksLikeFilePath(text: string): boolean {
|
|
14
20
|
const after = String(text || '').slice(1).trim();
|
|
15
21
|
const tok = after.split(/\s+/)[0] || '';
|
|
16
22
|
return tok.includes('/') || tok.includes('\\');
|
|
17
23
|
}
|
|
18
24
|
|
|
19
|
-
const dropdown = () => document.getElementById('cmdDropdown');
|
|
20
|
-
const input = () => document.getElementById('chatInput');
|
|
25
|
+
const dropdown = (): HTMLElement | null => document.getElementById('cmdDropdown');
|
|
26
|
+
const input = (): HTMLTextAreaElement | null => document.getElementById('chatInput') as HTMLTextAreaElement | null;
|
|
21
27
|
|
|
22
|
-
function escapeHtml(str) {
|
|
28
|
+
function escapeHtml(str: string): string {
|
|
23
29
|
return String(str || '')
|
|
24
30
|
.replace(/&/g, '&')
|
|
25
31
|
.replace(/</g, '<')
|
|
@@ -27,12 +33,12 @@ function escapeHtml(str) {
|
|
|
27
33
|
.replace(/"/g, '"');
|
|
28
34
|
}
|
|
29
35
|
|
|
30
|
-
function filterCommands(partial) {
|
|
36
|
+
function filterCommands(partial: string): SlashCommand[] {
|
|
31
37
|
const prefix = String(partial || '').toLowerCase();
|
|
32
38
|
return cmdList.filter(c => (`/${c.name}`).startsWith(prefix));
|
|
33
39
|
}
|
|
34
40
|
|
|
35
|
-
function showDropdown() {
|
|
41
|
+
function showDropdown(): void {
|
|
36
42
|
const el = dropdown();
|
|
37
43
|
const inp = input();
|
|
38
44
|
if (!el || !inp) return;
|
|
@@ -45,18 +51,16 @@ function showDropdown() {
|
|
|
45
51
|
el.style.display = 'block';
|
|
46
52
|
requestAnimationFrame(() => el.classList.add('visible'));
|
|
47
53
|
isOpen = true;
|
|
48
|
-
|
|
49
54
|
inp.setAttribute('aria-expanded', 'true');
|
|
50
55
|
}
|
|
51
56
|
|
|
52
|
-
function render() {
|
|
57
|
+
function render(): void {
|
|
53
58
|
const el = dropdown();
|
|
54
59
|
const inp = input();
|
|
55
60
|
if (!el || !inp) return;
|
|
56
61
|
|
|
57
62
|
if (!filtered.length) {
|
|
58
63
|
if (!inp.value.startsWith('/') || looksLikeFilePath(inp.value)) { close(); return; }
|
|
59
|
-
|
|
60
64
|
el.innerHTML = `
|
|
61
65
|
<div class="cmd-item cmd-empty" role="option" aria-disabled="true">
|
|
62
66
|
${t('cmd.noMatch')}
|
|
@@ -83,11 +87,11 @@ function render() {
|
|
|
83
87
|
showDropdown();
|
|
84
88
|
inp.setAttribute('aria-activedescendant', selectedIdx >= 0 ? `cmd-item-${selectedIdx}` : '');
|
|
85
89
|
|
|
86
|
-
const
|
|
87
|
-
if (
|
|
90
|
+
const selectedEl = el.querySelector('.selected');
|
|
91
|
+
if (selectedEl) selectedEl.scrollIntoView({ block: 'nearest' });
|
|
88
92
|
}
|
|
89
93
|
|
|
90
|
-
function applySelection(execute) {
|
|
94
|
+
function applySelection(execute: boolean): void {
|
|
91
95
|
const cmd = filtered[selectedIdx];
|
|
92
96
|
const inp = input();
|
|
93
97
|
if (!cmd || !inp) { close(); return; }
|
|
@@ -104,19 +108,19 @@ function applySelection(execute) {
|
|
|
104
108
|
inp.dispatchEvent(new Event('cmd-execute', { bubbles: true }));
|
|
105
109
|
}
|
|
106
110
|
|
|
107
|
-
export async function loadCommands() {
|
|
111
|
+
export async function loadCommands(): Promise<void> {
|
|
108
112
|
try {
|
|
109
113
|
const locale = getPreferredLocale();
|
|
110
114
|
const url = `/api/commands?interface=web&locale=${encodeURIComponent(locale)}`;
|
|
111
|
-
const data = await api(url, { headers: { 'Accept-Language': locale } });
|
|
115
|
+
const data = await api<SlashCommand[]>(url, { headers: { 'Accept-Language': locale } });
|
|
112
116
|
cmdList = data || [];
|
|
113
117
|
} catch (err) {
|
|
114
|
-
console.warn('[slash-commands] loadCommands failed:', err.message);
|
|
118
|
+
console.warn('[slash-commands] loadCommands failed:', (err as Error).message);
|
|
115
119
|
cmdList = [];
|
|
116
120
|
}
|
|
117
121
|
}
|
|
118
122
|
|
|
119
|
-
export function close() {
|
|
123
|
+
export function close(): void {
|
|
120
124
|
const el = dropdown();
|
|
121
125
|
|
|
122
126
|
if (closeTimer) {
|
|
@@ -143,19 +147,18 @@ export function close() {
|
|
|
143
147
|
}
|
|
144
148
|
}
|
|
145
149
|
|
|
146
|
-
export function update(text) {
|
|
150
|
+
export function update(text: string): void {
|
|
147
151
|
const raw = String(text || '');
|
|
148
152
|
if (!raw.startsWith('/') || raw.includes(' ') || raw.includes('\n') || looksLikeFilePath(raw)) {
|
|
149
153
|
close();
|
|
150
154
|
return;
|
|
151
155
|
}
|
|
152
|
-
|
|
153
156
|
filtered = filterCommands(raw);
|
|
154
157
|
selectedIdx = filtered.length ? 0 : -1;
|
|
155
158
|
render();
|
|
156
159
|
}
|
|
157
160
|
|
|
158
|
-
export function handleKeydown(e) {
|
|
161
|
+
export function handleKeydown(e: KeyboardEvent): boolean {
|
|
159
162
|
if (!isOpen) {
|
|
160
163
|
if ((e.key === 'ArrowDown' || e.key === 'ArrowUp') && !e.isComposing) {
|
|
161
164
|
update(input()?.value || '');
|
|
@@ -207,8 +210,9 @@ export function handleKeydown(e) {
|
|
|
207
210
|
return false;
|
|
208
211
|
}
|
|
209
212
|
|
|
210
|
-
export function handleClick(e) {
|
|
211
|
-
const
|
|
213
|
+
export function handleClick(e: Event): void {
|
|
214
|
+
const target = e.target as HTMLElement;
|
|
215
|
+
const item = target.closest('.cmd-item') as HTMLElement | null;
|
|
212
216
|
if (!item) return;
|
|
213
217
|
const idx = parseInt(item.dataset.index || '-1', 10);
|
|
214
218
|
if (Number.isNaN(idx) || idx < 0) return;
|
|
@@ -216,16 +220,17 @@ export function handleClick(e) {
|
|
|
216
220
|
applySelection(true);
|
|
217
221
|
}
|
|
218
222
|
|
|
219
|
-
export function handleOutsideClick(e) {
|
|
223
|
+
export function handleOutsideClick(e: Event): void {
|
|
220
224
|
if (!isOpen) return;
|
|
221
225
|
const el = dropdown();
|
|
222
226
|
const inp = input();
|
|
223
227
|
if (!el || !inp) return;
|
|
224
|
-
|
|
228
|
+
const target = e.target as Node;
|
|
229
|
+
if (!el.contains(target) && target !== inp) {
|
|
225
230
|
close();
|
|
226
231
|
}
|
|
227
232
|
}
|
|
228
233
|
|
|
229
|
-
export function isDropdownOpen() {
|
|
234
|
+
export function isDropdownOpen(): boolean {
|
|
230
235
|
return isOpen;
|
|
231
236
|
}
|
|
@@ -6,7 +6,7 @@ const STORAGE_KEY = 'theme';
|
|
|
6
6
|
const HLJS_DARK = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github-dark.min.css';
|
|
7
7
|
const HLJS_LIGHT = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.11.1/styles/github.min.css';
|
|
8
8
|
|
|
9
|
-
export function initTheme() {
|
|
9
|
+
export function initTheme(): void {
|
|
10
10
|
// Detect: localStorage → OS preference → default dark
|
|
11
11
|
const saved = localStorage.getItem(STORAGE_KEY);
|
|
12
12
|
const prefer = window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
|
@@ -16,14 +16,14 @@ export function initTheme() {
|
|
|
16
16
|
document.getElementById('toggleTheme')?.addEventListener('click', toggleTheme);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
function toggleTheme() {
|
|
19
|
+
function toggleTheme(): void {
|
|
20
20
|
const current = document.documentElement.getAttribute('data-theme') || 'dark';
|
|
21
21
|
const next = current === 'dark' ? 'light' : 'dark';
|
|
22
22
|
applyTheme(next);
|
|
23
23
|
localStorage.setItem(STORAGE_KEY, next);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
function applyTheme(theme) {
|
|
26
|
+
function applyTheme(theme: string): void {
|
|
27
27
|
document.documentElement.setAttribute('data-theme', theme);
|
|
28
28
|
|
|
29
29
|
// Update button icon (SVG sun/moon swap)
|
|
@@ -33,7 +33,7 @@ function applyTheme(theme) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// Swap highlight.js theme
|
|
36
|
-
const hljsLink = document.getElementById('hljsTheme');
|
|
36
|
+
const hljsLink = document.getElementById('hljsTheme') as HTMLLinkElement | null;
|
|
37
37
|
if (hljsLink) {
|
|
38
38
|
hljsLink.href = theme === 'dark' ? HLJS_DARK : HLJS_LIGHT;
|
|
39
39
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// ── Web Locale Helpers ──
|
|
2
2
|
|
|
3
|
-
const LOCALE_KEYS = ['claw_locale', 'claw.locale'];
|
|
3
|
+
const LOCALE_KEYS: string[] = ['claw_locale', 'claw.locale'];
|
|
4
4
|
|
|
5
|
-
export function getPreferredLocale() {
|
|
5
|
+
export function getPreferredLocale(): string {
|
|
6
6
|
try {
|
|
7
7
|
for (const key of LOCALE_KEYS) {
|
|
8
8
|
const saved = localStorage.getItem(key);
|
|
@@ -12,7 +12,7 @@ export function getPreferredLocale() {
|
|
|
12
12
|
return navigator.language || 'ko';
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export function syncStoredLocale(locale) {
|
|
15
|
+
export function syncStoredLocale(locale: string): void {
|
|
16
16
|
const value = String(locale || '').trim();
|
|
17
17
|
if (!value) return;
|
|
18
18
|
try {
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// ── App Entry Point ──
|
|
2
|
+
// All event bindings happen here (no inline onclick in HTML)
|
|
3
|
+
|
|
4
|
+
// ── Global Error Boundary ──
|
|
5
|
+
window.addEventListener('unhandledrejection', (e) => {
|
|
6
|
+
console.error('[unhandled]', e.reason);
|
|
7
|
+
e.preventDefault();
|
|
8
|
+
});
|
|
9
|
+
window.addEventListener('error', (e) => {
|
|
10
|
+
console.error('[error]', e.message, e.filename, e.lineno);
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
import { connect } from './ws.js';
|
|
14
|
+
import { switchTab, handleSave, loadStats, loadMessages, loadMemory, initMsgCopy } from './ui.js';
|
|
15
|
+
import { sendMessage, handleKey, clearAttachedFiles, removeAttachedFile, clearChat, initDragDrop, initAutoResize } from './features/chat.js';
|
|
16
|
+
import {
|
|
17
|
+
loadCommands, update as updateSlashDropdown, handleKeydown as handleSlashKeydown,
|
|
18
|
+
handleClick as handleSlashClick, handleOutsideClick as handleSlashOutsideClick,
|
|
19
|
+
} from './features/slash-commands.js';
|
|
20
|
+
import { loadSkills, toggleSkill, filterSkills } from './features/skills.js';
|
|
21
|
+
import {
|
|
22
|
+
loadSettings, setPerm, handleModelSelect, applyCustomModel, onCliChange,
|
|
23
|
+
saveActiveCliSettings, savePerCli, updateSettings, openPromptModal,
|
|
24
|
+
closePromptModal, savePromptFromModal, syncMcpServers, installMcpGlobal,
|
|
25
|
+
loadCliStatus, setTelegram, saveTelegramSettings, saveFallbackOrder
|
|
26
|
+
} from './features/settings.js';
|
|
27
|
+
import {
|
|
28
|
+
loadEmployees, addEmployee, deleteEmployee, updateEmployee,
|
|
29
|
+
onEmpCliChange, onEmpRoleChange
|
|
30
|
+
} from './features/employees.js';
|
|
31
|
+
import {
|
|
32
|
+
openHeartbeatModal, closeHeartbeatModal, addHeartbeatJob,
|
|
33
|
+
removeHeartbeatJob, toggleHeartbeatJob, saveHeartbeatJobs,
|
|
34
|
+
initHeartbeatBadge
|
|
35
|
+
} from './features/heartbeat.js';
|
|
36
|
+
import {
|
|
37
|
+
openMemoryModal, closeMemoryModal, switchMemTab, setMemEnabled,
|
|
38
|
+
saveMemSettings, deleteMemFile, viewMemFile
|
|
39
|
+
} from './features/memory.js';
|
|
40
|
+
import { state } from './state.js';
|
|
41
|
+
import { loadCliRegistry, getCliKeys } from './constants.js';
|
|
42
|
+
import { initAppName } from './features/appname.js';
|
|
43
|
+
import { initSidebar, toggleLeft, toggleRight } from './features/sidebar.js';
|
|
44
|
+
import { initTheme } from './features/theme.js';
|
|
45
|
+
import { initI18n, setLang, getLang, t } from './features/i18n.js';
|
|
46
|
+
|
|
47
|
+
// ── Chat Actions ──
|
|
48
|
+
document.getElementById('btnSend')?.addEventListener('click', sendMessage);
|
|
49
|
+
const chatInput = document.getElementById('chatInput') as HTMLTextAreaElement | null;
|
|
50
|
+
chatInput?.addEventListener('keydown', (e) => {
|
|
51
|
+
if (handleSlashKeydown(e as KeyboardEvent)) return;
|
|
52
|
+
handleKey(e as KeyboardEvent);
|
|
53
|
+
});
|
|
54
|
+
let slashInputRaf = 0;
|
|
55
|
+
chatInput?.addEventListener('input', (e) => {
|
|
56
|
+
if ((e as InputEvent).isComposing) return;
|
|
57
|
+
if (slashInputRaf) cancelAnimationFrame(slashInputRaf);
|
|
58
|
+
slashInputRaf = requestAnimationFrame(() => {
|
|
59
|
+
updateSlashDropdown((e.target as HTMLTextAreaElement)?.value || '');
|
|
60
|
+
slashInputRaf = 0;
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
chatInput?.addEventListener('cmd-execute', () => {
|
|
64
|
+
void sendMessage();
|
|
65
|
+
});
|
|
66
|
+
document.getElementById('cmdDropdown')?.addEventListener('click', handleSlashClick);
|
|
67
|
+
document.addEventListener('click', handleSlashOutsideClick);
|
|
68
|
+
document.getElementById('filePreviewClear')?.addEventListener('click', clearAttachedFiles);
|
|
69
|
+
document.getElementById('filePreviewList')?.addEventListener('click', (e) => {
|
|
70
|
+
const btn = (e.target as HTMLElement)?.closest('[data-file-idx]') as HTMLElement | null;
|
|
71
|
+
if (btn) removeAttachedFile(+(btn.dataset.fileIdx || '0'));
|
|
72
|
+
});
|
|
73
|
+
document.querySelector('.btn-attach')?.addEventListener('click', () => {
|
|
74
|
+
(document.getElementById('fileInput') as HTMLInputElement | null)?.click();
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
// ── Left Sidebar ──
|
|
78
|
+
document.getElementById('memorySidebarBtn')?.addEventListener('click', openMemoryModal);
|
|
79
|
+
document.getElementById('btnClearChat')?.addEventListener('click', clearChat);
|
|
80
|
+
document.getElementById('hbSidebarBtn')?.addEventListener('click', openHeartbeatModal);
|
|
81
|
+
|
|
82
|
+
// Language toggle
|
|
83
|
+
document.getElementById('langToggle')?.addEventListener('click', async () => {
|
|
84
|
+
const next = getLang() === 'ko' ? 'en' : 'ko';
|
|
85
|
+
await setLang(next);
|
|
86
|
+
const btn = document.getElementById('langToggle');
|
|
87
|
+
if (btn) btn.textContent = `🌐 ${t('lang.' + next)}`;
|
|
88
|
+
// Reconnect WS with new locale
|
|
89
|
+
if (state.ws) { state.ws.close(); }
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
// ── Tab Bar (event delegation) ──
|
|
93
|
+
document.querySelector('.tab-bar')?.addEventListener('click', (e) => {
|
|
94
|
+
const btn = (e.target as HTMLElement)?.closest('.tab-btn') as HTMLElement | null;
|
|
95
|
+
if (!btn) return;
|
|
96
|
+
const tabs = [...(btn.parentElement?.children || [])].filter(c => c.classList.contains('tab-btn'));
|
|
97
|
+
const idx = tabs.indexOf(btn);
|
|
98
|
+
const names = ['agents', 'skills', 'settings'];
|
|
99
|
+
if (names[idx]) switchTab(names[idx], btn);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// ── Save Button ──
|
|
103
|
+
document.querySelector('.sidebar-save-bar .btn-save')?.addEventListener('click', handleSave);
|
|
104
|
+
|
|
105
|
+
// ── Agents Tab ──
|
|
106
|
+
document.getElementById('selCli')?.addEventListener('change', () => onCliChange());
|
|
107
|
+
document.getElementById('selModel')?.addEventListener('change', () => saveActiveCliSettings());
|
|
108
|
+
document.getElementById('selEffort')?.addEventListener('change', () => saveActiveCliSettings());
|
|
109
|
+
document.querySelector('[data-action="addEmployee"]')?.addEventListener('click', addEmployee);
|
|
110
|
+
|
|
111
|
+
// ── Employees (Event Delegation) ──
|
|
112
|
+
document.getElementById('employeesList')?.addEventListener('click', (e) => {
|
|
113
|
+
const del = (e.target as HTMLElement)?.closest('[data-emp-delete]') as HTMLElement | null;
|
|
114
|
+
if (del) { deleteEmployee(del.dataset.empDelete || ''); return; }
|
|
115
|
+
});
|
|
116
|
+
document.getElementById('employeesList')?.addEventListener('change', (e) => {
|
|
117
|
+
const tgt = e.target as HTMLSelectElement;
|
|
118
|
+
const name = tgt.closest('[data-emp-name]') as HTMLElement | null;
|
|
119
|
+
if (name) { updateEmployee(name.dataset.empName || '', { name: tgt.value }); return; }
|
|
120
|
+
const cli = tgt.closest('[data-emp-cli]') as HTMLElement | null;
|
|
121
|
+
if (cli) { onEmpCliChange(cli.dataset.empCli || '', tgt.value); return; }
|
|
122
|
+
const model = tgt.closest('[data-emp-model]') as HTMLElement | null;
|
|
123
|
+
if (model) {
|
|
124
|
+
if (tgt.value === '__custom__') {
|
|
125
|
+
const val = prompt(t('model.promptInput'));
|
|
126
|
+
if (val?.trim()) {
|
|
127
|
+
const opt = document.createElement('option');
|
|
128
|
+
opt.value = val.trim(); opt.textContent = val.trim();
|
|
129
|
+
const customOpt = tgt.querySelector('option[value="__custom__"]');
|
|
130
|
+
if (customOpt) tgt.insertBefore(opt, customOpt);
|
|
131
|
+
tgt.value = val.trim();
|
|
132
|
+
updateEmployee(model.dataset.empModel || '', { model: val.trim() });
|
|
133
|
+
} else { tgt.value = 'default'; }
|
|
134
|
+
} else { updateEmployee(model.dataset.empModel || '', { model: tgt.value }); }
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
const role = tgt.closest('[data-emp-role]') as HTMLElement | null;
|
|
138
|
+
if (role) { onEmpRoleChange(role.dataset.empRole || '', tgt.value); return; }
|
|
139
|
+
const custom = tgt.closest('[data-emp-custom]') as HTMLElement | null;
|
|
140
|
+
if (custom) { updateEmployee(custom.dataset.empCustom || '', { role: tgt.value }); return; }
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// ── Skills Tab (Event Delegation) ──
|
|
144
|
+
document.getElementById('skillsList')?.addEventListener('click', (e) => {
|
|
145
|
+
const toggle = (e.target as HTMLElement)?.closest('[data-skill-id]') as HTMLElement | null;
|
|
146
|
+
if (toggle) {
|
|
147
|
+
toggleSkill(toggle.dataset.skillId || '', toggle.dataset.skillEnabled === 'true');
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
// Skill filter buttons (event delegation)
|
|
151
|
+
document.querySelector('#tabSkills')?.addEventListener('click', (e) => {
|
|
152
|
+
const filterBtn = (e.target as HTMLElement)?.closest('.skill-filter') as HTMLElement | null;
|
|
153
|
+
if (filterBtn) {
|
|
154
|
+
const cat = filterBtn.dataset.filter || 'all';
|
|
155
|
+
filterSkills(cat, filterBtn);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// ── Settings Tab ──
|
|
160
|
+
document.querySelector('[data-action="openPrompt"]')?.addEventListener('click', openPromptModal);
|
|
161
|
+
document.getElementById('tgOff')?.addEventListener('click', () => setTelegram(false));
|
|
162
|
+
document.getElementById('tgOn')?.addEventListener('click', () => setTelegram(true));
|
|
163
|
+
document.getElementById('tgToken')?.addEventListener('change', saveTelegramSettings);
|
|
164
|
+
document.getElementById('tgChatIds')?.addEventListener('change', saveTelegramSettings);
|
|
165
|
+
document.getElementById('fallbackOrderList')?.addEventListener('change', saveFallbackOrder);
|
|
166
|
+
|
|
167
|
+
// Per-CLI model selects
|
|
168
|
+
function bindPerCliControlEvents(): void {
|
|
169
|
+
for (const cli of getCliKeys()) {
|
|
170
|
+
const cap = cli.charAt(0).toUpperCase() + cli.slice(1);
|
|
171
|
+
const sel = document.getElementById('model' + cap) as HTMLSelectElement | null;
|
|
172
|
+
if (sel) sel.addEventListener('change', function (this: HTMLSelectElement) { handleModelSelect(cli, this); });
|
|
173
|
+
const custom = document.getElementById('customModel' + cap) as HTMLInputElement | null;
|
|
174
|
+
if (custom) custom.addEventListener('change', function (this: HTMLInputElement) { applyCustomModel(cli, this); });
|
|
175
|
+
const effort = document.getElementById('effort' + cap);
|
|
176
|
+
if (effort) effort.addEventListener('change', savePerCli);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// MCP
|
|
181
|
+
document.querySelector('[data-action="syncMcp"]')?.addEventListener('click', syncMcpServers);
|
|
182
|
+
document.querySelector('[data-action="installMcp"]')?.addEventListener('click', installMcpGlobal);
|
|
183
|
+
document.querySelector('[data-action="refreshCli"]')?.addEventListener('click', () => loadCliStatus(true));
|
|
184
|
+
document.getElementById('cliStatusInterval')?.addEventListener('change', function (this: HTMLSelectElement) {
|
|
185
|
+
localStorage.setItem('cliStatusInterval', this.value);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// ── Prompt Modal ──
|
|
189
|
+
document.getElementById('promptModal')?.addEventListener('click', (e) => closePromptModal(e));
|
|
190
|
+
document.querySelector('#promptModal .modal-box')?.addEventListener('click', (e) => e.stopPropagation());
|
|
191
|
+
document.querySelector('[data-action="closePrompt"]')?.addEventListener('click', () => closePromptModal());
|
|
192
|
+
document.querySelector('[data-action="cancelPrompt"]')?.addEventListener('click', () => closePromptModal());
|
|
193
|
+
document.querySelector('[data-action="savePrompt"]')?.addEventListener('click', savePromptFromModal);
|
|
194
|
+
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') closePromptModal(); });
|
|
195
|
+
|
|
196
|
+
// ── Heartbeat Modal ──
|
|
197
|
+
document.getElementById('heartbeatModal')?.addEventListener('click', (e) => closeHeartbeatModal(e));
|
|
198
|
+
document.querySelector('#heartbeatModal .modal-box')?.addEventListener('click', (e) => e.stopPropagation());
|
|
199
|
+
document.querySelector('[data-action="closeHeartbeat"]')?.addEventListener('click', () => closeHeartbeatModal());
|
|
200
|
+
document.querySelector('[data-action="addHeartbeat"]')?.addEventListener('click', addHeartbeatJob);
|
|
201
|
+
|
|
202
|
+
// Heartbeat jobs (event delegation)
|
|
203
|
+
document.getElementById('hbJobsList')?.addEventListener('click', (e) => {
|
|
204
|
+
const toggle = (e.target as HTMLElement)?.closest('[data-hb-toggle]') as HTMLElement | null;
|
|
205
|
+
if (toggle) { toggleHeartbeatJob(+(toggle.dataset.hbToggle || '0')); return; }
|
|
206
|
+
const remove = (e.target as HTMLElement)?.closest('[data-hb-remove]') as HTMLElement | null;
|
|
207
|
+
if (remove) { removeHeartbeatJob(+(remove.dataset.hbRemove || '0')); return; }
|
|
208
|
+
});
|
|
209
|
+
document.getElementById('hbJobsList')?.addEventListener('change', (e) => {
|
|
210
|
+
const tgt = e.target as HTMLInputElement;
|
|
211
|
+
const name = tgt.closest('[data-hb-name]') as HTMLElement | null;
|
|
212
|
+
if (name) { state.heartbeatJobs[+(name.dataset.hbName || '0')].name = tgt.value; saveHeartbeatJobs(); return; }
|
|
213
|
+
const min = tgt.closest('[data-hb-minutes]') as HTMLElement | null;
|
|
214
|
+
if (min) { state.heartbeatJobs[+(min.dataset.hbMinutes || '0')].schedule = { kind: 'every', minutes: +tgt.value }; saveHeartbeatJobs(); return; }
|
|
215
|
+
const prompt = tgt.closest('[data-hb-prompt]') as HTMLElement | null;
|
|
216
|
+
if (prompt) { state.heartbeatJobs[+(prompt.dataset.hbPrompt || '0')].prompt = tgt.value; saveHeartbeatJobs(); return; }
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// ── Memory Modal ──
|
|
220
|
+
document.getElementById('memoryModal')?.addEventListener('click', (e) => closeMemoryModal(e));
|
|
221
|
+
document.querySelector('#memoryModal .modal-box')?.addEventListener('click', (e) => e.stopPropagation());
|
|
222
|
+
document.querySelector('[data-action="closeMemory"]')?.addEventListener('click', () => closeMemoryModal());
|
|
223
|
+
document.getElementById('memTabBtnSettings')?.addEventListener('click', () => switchMemTab('settings'));
|
|
224
|
+
document.getElementById('memTabBtnFiles')?.addEventListener('click', () => switchMemTab('files'));
|
|
225
|
+
document.getElementById('memOn')?.addEventListener('click', () => setMemEnabled(true));
|
|
226
|
+
document.getElementById('memOff')?.addEventListener('click', () => setMemEnabled(false));
|
|
227
|
+
document.getElementById('memFlushEvery')?.addEventListener('change', saveMemSettings);
|
|
228
|
+
document.getElementById('memCli')?.addEventListener('change', saveMemSettings);
|
|
229
|
+
document.getElementById('memModel')?.addEventListener('change', saveMemSettings);
|
|
230
|
+
document.getElementById('memRetention')?.addEventListener('change', saveMemSettings);
|
|
231
|
+
|
|
232
|
+
// Memory files (event delegation)
|
|
233
|
+
document.getElementById('memFilesList')?.addEventListener('click', (e) => {
|
|
234
|
+
const del = (e.target as HTMLElement)?.closest('[data-mem-delete]') as HTMLElement | null;
|
|
235
|
+
if (del) { e.stopPropagation(); deleteMemFile(del.dataset.memDelete || ''); return; }
|
|
236
|
+
const view = (e.target as HTMLElement)?.closest('[data-mem-view]') as HTMLElement | null;
|
|
237
|
+
if (view) { viewMemFile(view.dataset.memView || ''); return; }
|
|
238
|
+
const back = (e.target as HTMLElement)?.closest('[data-mem-back]');
|
|
239
|
+
if (back) { openMemoryModal(); return; }
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
// ── Init ──
|
|
243
|
+
async function bootstrap(): Promise<void> {
|
|
244
|
+
await initI18n();
|
|
245
|
+
const langBtn = document.getElementById('langToggle');
|
|
246
|
+
if (langBtn) langBtn.textContent = `🌐 ${t('lang.' + getLang())}`;
|
|
247
|
+
await loadCliRegistry();
|
|
248
|
+
bindPerCliControlEvents();
|
|
249
|
+
connect();
|
|
250
|
+
initDragDrop();
|
|
251
|
+
initAutoResize();
|
|
252
|
+
await loadCommands();
|
|
253
|
+
await loadSettings();
|
|
254
|
+
loadCliStatus();
|
|
255
|
+
loadMemory();
|
|
256
|
+
// loadMessages() is handled by ws.js onopen (clear + reload)
|
|
257
|
+
loadEmployees();
|
|
258
|
+
initHeartbeatBadge();
|
|
259
|
+
initAppName();
|
|
260
|
+
initSidebar();
|
|
261
|
+
initTheme();
|
|
262
|
+
initMsgCopy();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
void bootstrap().catch((err: unknown) => {
|
|
266
|
+
console.error('[bootstrap]', err);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
// ── Keyboard: Escape closes modals ──────────────────
|
|
270
|
+
document.addEventListener('keydown', (e) => {
|
|
271
|
+
if (e.key === 'Escape') {
|
|
272
|
+
document.querySelectorAll('.modal-overlay.open').forEach(m => {
|
|
273
|
+
m.classList.remove('open');
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// ── Mobile sidebar toggle (sidebar.js functions reuse) ──
|
|
279
|
+
document.getElementById('mobileMenuLeft')?.addEventListener('click', toggleLeft);
|
|
280
|
+
document.getElementById('mobileMenuRight')?.addEventListener('click', toggleRight);
|