cli-jaw 0.1.11 → 1.0.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.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
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
// ── Render Helpers ──
|
|
2
2
|
// Modular markdown rendering: marked.js + highlight.js + KaTeX + Mermaid
|
|
3
3
|
// All libs loaded via CDN (defer), graceful fallback if unavailable
|
|
4
|
+
|
|
4
5
|
import { t } from './features/i18n.js';
|
|
5
6
|
|
|
6
|
-
export function escapeHtml(str) {
|
|
7
|
+
export function escapeHtml(str: string): string {
|
|
7
8
|
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')
|
|
8
9
|
.replace(/"/g, '"').replace(/'/g, ''');
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
// ── XSS sanitization ──
|
|
12
|
-
export function sanitizeHtml(html) {
|
|
13
|
+
export function sanitizeHtml(html: string): string {
|
|
13
14
|
if (typeof DOMPurify !== 'undefined') {
|
|
14
15
|
return DOMPurify.sanitize(html, {
|
|
15
|
-
USE_PROFILES: { html: true },
|
|
16
|
+
USE_PROFILES: { html: true, svg: true, svgFilters: true },
|
|
16
17
|
FORBID_TAGS: ['script', 'style', 'iframe', 'object', 'embed', 'form'],
|
|
17
18
|
FORBID_ATTR: ['onerror', 'onclick', 'onload', 'onmouseover', 'onfocus', 'onblur'],
|
|
18
19
|
ADD_TAGS: ['use'], // Mermaid SVG compatibility
|
|
@@ -26,23 +27,23 @@ export function sanitizeHtml(html) {
|
|
|
26
27
|
}
|
|
27
28
|
|
|
28
29
|
// ── Orchestration JSON stripping ──
|
|
29
|
-
function stripOrchestration(text) {
|
|
30
|
+
function stripOrchestration(text: string): string {
|
|
30
31
|
let cleaned = text.replace(/```json\n[\s\S]*?\n```/g, '');
|
|
31
32
|
cleaned = cleaned.replace(/\{[\s\S]*"subtasks"\s*:\s*\[[\s\S]*?\]\s*\}/g, '').trim();
|
|
32
33
|
return cleaned;
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
// ── KaTeX inline/block math ──
|
|
36
|
-
function renderMath(html) {
|
|
37
|
+
function renderMath(html: string): string {
|
|
37
38
|
if (typeof katex === 'undefined') return html;
|
|
38
39
|
// Block math: $$...$$
|
|
39
|
-
html = html.replace(/\$\$([\s\S]+?)\$\$/g, (_, tex) => {
|
|
40
|
+
html = html.replace(/\$\$([\s\S]+?)\$\$/g, (_: string, tex: string) => {
|
|
40
41
|
try {
|
|
41
42
|
return katex.renderToString(tex.trim(), { displayMode: true, throwOnError: false });
|
|
42
43
|
} catch { return `<code>${escapeHtml(tex)}</code>`; }
|
|
43
44
|
});
|
|
44
45
|
// Inline math: $...$ (avoid matching currency like $10)
|
|
45
|
-
html = html.replace(/(?<!\$)\$(?!\$)([^\n$]+?)\$(?!\$)/g, (_, tex) => {
|
|
46
|
+
html = html.replace(/(?<!\$)\$(?!\$)([^\n$]+?)\$(?!\$)/g, (_: string, tex: string) => {
|
|
46
47
|
try {
|
|
47
48
|
return katex.renderToString(tex.trim(), { displayMode: false, throwOnError: false });
|
|
48
49
|
} catch { return `<code>${escapeHtml(tex)}</code>`; }
|
|
@@ -51,89 +52,42 @@ function renderMath(html) {
|
|
|
51
52
|
}
|
|
52
53
|
|
|
53
54
|
// ── Mermaid deferred rendering ──
|
|
54
|
-
// NOTE: No DOMPurify on mermaid SVG — mermaid.render() with securityLevel:'loose'
|
|
55
|
-
// handles its own sanitization. DOMPurify strips foreignObject/style tags needed for text.
|
|
56
55
|
let mermaidId = 0;
|
|
57
56
|
|
|
58
|
-
function renderMermaidBlocks() {
|
|
57
|
+
function renderMermaidBlocks(): void {
|
|
59
58
|
if (typeof mermaid === 'undefined') return;
|
|
60
59
|
document.querySelectorAll('.mermaid-pending').forEach(async (el) => {
|
|
61
60
|
el.classList.remove('mermaid-pending');
|
|
62
|
-
const code = el.textContent;
|
|
61
|
+
const code = el.textContent || '';
|
|
63
62
|
const id = `mermaid-${++mermaidId}`;
|
|
64
63
|
try {
|
|
65
64
|
const { svg } = await mermaid.render(id, code);
|
|
66
|
-
el.innerHTML = svg;
|
|
65
|
+
el.innerHTML = sanitizeHtml(svg);
|
|
67
66
|
el.classList.add('mermaid-rendered');
|
|
68
|
-
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
const zoomBtn = document.createElement('button');
|
|
72
|
-
zoomBtn.className = 'mermaid-zoom-btn';
|
|
73
|
-
zoomBtn.textContent = '⛶';
|
|
74
|
-
zoomBtn.title = 'Expand diagram';
|
|
75
|
-
zoomBtn.addEventListener('click', () => openMermaidOverlay(rawSvg));
|
|
76
|
-
el.appendChild(zoomBtn);
|
|
77
|
-
} catch (err) {
|
|
78
|
-
const errMsg = err?.message || err?.str || 'Unknown error';
|
|
67
|
+
} catch (err: unknown) {
|
|
68
|
+
const errMsg = (err as { message?: string; str?: string })?.message
|
|
69
|
+
|| (err as { str?: string })?.str || 'Unknown error';
|
|
79
70
|
el.innerHTML = `
|
|
80
71
|
<div style="border:1px solid #ef4444;border-radius:6px;padding:8px;margin:4px 0">
|
|
81
|
-
<div style="color:#ef4444;font-size:11px;margin-bottom:4px">⚠️ Mermaid
|
|
72
|
+
<div style="color:#ef4444;font-size:11px;margin-bottom:4px">⚠️ ${escapeHtml(t('mermaid.renderFail') || 'Mermaid render failed')}</div>
|
|
82
73
|
<div style="color:#fbbf24;font-size:10px;margin-bottom:6px">${escapeHtml(errMsg.slice(0, 200))}</div>
|
|
83
74
|
<pre style="margin:0;font-size:11px;overflow-x:auto"><code>${escapeHtml(code)}</code></pre>
|
|
84
75
|
</div>`;
|
|
85
76
|
}
|
|
86
77
|
});
|
|
87
78
|
}
|
|
88
|
-
// ── Mermaid popup overlay ──
|
|
89
|
-
function openMermaidOverlay(svgHtml) {
|
|
90
|
-
// Remove existing overlay if any
|
|
91
|
-
document.getElementById('mermaidOverlay')?.remove();
|
|
92
|
-
|
|
93
|
-
const overlay = document.createElement('div');
|
|
94
|
-
overlay.id = 'mermaidOverlay';
|
|
95
|
-
overlay.className = 'mermaid-overlay';
|
|
96
|
-
overlay.innerHTML = `
|
|
97
|
-
<div class="mermaid-overlay-backdrop"></div>
|
|
98
|
-
<div class="mermaid-overlay-content">
|
|
99
|
-
<button class="mermaid-overlay-close">✕</button>
|
|
100
|
-
<div class="mermaid-overlay-svg">${svgHtml}</div>
|
|
101
|
-
</div>`;
|
|
102
|
-
document.body.appendChild(overlay);
|
|
103
|
-
|
|
104
|
-
// Make SVG fill the popup
|
|
105
|
-
const svgEl = overlay.querySelector('svg');
|
|
106
|
-
if (svgEl) {
|
|
107
|
-
svgEl.removeAttribute('width');
|
|
108
|
-
svgEl.removeAttribute('height');
|
|
109
|
-
svgEl.style.width = '100%';
|
|
110
|
-
svgEl.style.height = 'auto';
|
|
111
|
-
svgEl.style.maxHeight = '85vh';
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
const close = () => overlay.remove();
|
|
115
|
-
overlay.querySelector('.mermaid-overlay-backdrop').addEventListener('click', close);
|
|
116
|
-
overlay.querySelector('.mermaid-overlay-close').addEventListener('click', (e) => {
|
|
117
|
-
e.stopPropagation();
|
|
118
|
-
e.preventDefault();
|
|
119
|
-
close();
|
|
120
|
-
});
|
|
121
|
-
document.addEventListener('keydown', function handler(e) {
|
|
122
|
-
if (e.key === 'Escape') { close(); document.removeEventListener('keydown', handler); }
|
|
123
|
-
});
|
|
124
|
-
}
|
|
125
79
|
|
|
126
80
|
// ── marked.js configuration ──
|
|
127
81
|
let markedReady = false;
|
|
128
82
|
|
|
129
|
-
function ensureMarked() {
|
|
83
|
+
function ensureMarked(): boolean {
|
|
130
84
|
if (markedReady) return true;
|
|
131
85
|
if (typeof marked === 'undefined') return false;
|
|
132
86
|
|
|
133
87
|
const renderer = new marked.Renderer();
|
|
134
88
|
|
|
135
89
|
// Code blocks: highlight.js + mermaid detection
|
|
136
|
-
renderer.code = function ({ text, lang }) {
|
|
90
|
+
renderer.code = function ({ text, lang }: { text: string; lang?: string }) {
|
|
137
91
|
// Mermaid
|
|
138
92
|
if (lang === 'mermaid') {
|
|
139
93
|
return `<div class="mermaid-container mermaid-pending">${escapeHtml(text)}</div>`;
|
|
@@ -164,25 +118,8 @@ function ensureMarked() {
|
|
|
164
118
|
if (typeof window.mermaid !== 'undefined') {
|
|
165
119
|
window.mermaid.initialize({
|
|
166
120
|
startOnLoad: false,
|
|
167
|
-
theme: '
|
|
168
|
-
securityLevel: '
|
|
169
|
-
themeVariables: {
|
|
170
|
-
darkMode: true,
|
|
171
|
-
background: '#0f172a',
|
|
172
|
-
primaryColor: '#1e3a5f',
|
|
173
|
-
primaryTextColor: '#e2e8f0',
|
|
174
|
-
primaryBorderColor: '#38bdf8',
|
|
175
|
-
lineColor: '#94a3b8',
|
|
176
|
-
secondaryColor: '#1e293b',
|
|
177
|
-
tertiaryColor: '#0f172a',
|
|
178
|
-
textColor: '#e2e8f0',
|
|
179
|
-
mainBkg: '#1e293b',
|
|
180
|
-
nodeBorder: '#38bdf8',
|
|
181
|
-
clusterBkg: '#1e293b',
|
|
182
|
-
titleColor: '#e2e8f0',
|
|
183
|
-
edgeLabelBackground: '#1e293b',
|
|
184
|
-
nodeTextColor: '#e2e8f0',
|
|
185
|
-
},
|
|
121
|
+
theme: 'dark',
|
|
122
|
+
securityLevel: 'strict',
|
|
186
123
|
});
|
|
187
124
|
}
|
|
188
125
|
|
|
@@ -191,7 +128,7 @@ function ensureMarked() {
|
|
|
191
128
|
}
|
|
192
129
|
|
|
193
130
|
// ── Fallback regex renderer (CDN 실패 시) ──
|
|
194
|
-
function renderFallback(text) {
|
|
131
|
+
function renderFallback(text: string): string {
|
|
195
132
|
return escapeHtml(text)
|
|
196
133
|
.replace(/`{3,}(\w*)\n([\s\S]*?)`{3,}/g, '<pre><code>$2</code></pre>')
|
|
197
134
|
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
@@ -203,54 +140,44 @@ function renderFallback(text) {
|
|
|
203
140
|
}
|
|
204
141
|
|
|
205
142
|
// ── Rehighlight all code blocks (call after hljs loads) ──
|
|
206
|
-
export function rehighlightAll() {
|
|
143
|
+
export function rehighlightAll(): void {
|
|
207
144
|
if (typeof hljs === 'undefined') return;
|
|
208
145
|
document.querySelectorAll('.code-block-wrapper pre code').forEach(el => {
|
|
209
|
-
if (el.dataset.highlighted === 'yes') return;
|
|
146
|
+
if ((el as HTMLElement).dataset.highlighted === 'yes') return;
|
|
210
147
|
const lang = [...el.classList].find(c => c.startsWith('language-'))?.replace('language-', '');
|
|
211
|
-
const raw = el.textContent;
|
|
148
|
+
const raw = el.textContent || '';
|
|
212
149
|
try {
|
|
213
150
|
if (lang && hljs.getLanguage(lang)) {
|
|
214
151
|
el.innerHTML = hljs.highlight(raw, { language: lang }).value;
|
|
215
152
|
} else {
|
|
216
153
|
el.innerHTML = hljs.highlightAuto(raw).value;
|
|
217
154
|
}
|
|
218
|
-
el.dataset.highlighted = 'yes';
|
|
155
|
+
(el as HTMLElement).dataset.highlighted = 'yes';
|
|
219
156
|
} catch { /* ignore */ }
|
|
220
157
|
});
|
|
221
158
|
}
|
|
222
159
|
|
|
223
160
|
// Poll for hljs load and auto-rehighlight
|
|
224
|
-
(function waitForHljs() {
|
|
161
|
+
(function waitForHljs(): void {
|
|
225
162
|
if (typeof hljs !== 'undefined') { rehighlightAll(); return; }
|
|
226
163
|
setTimeout(waitForHljs, 200);
|
|
227
164
|
})();
|
|
228
165
|
|
|
229
|
-
// Poll for mermaid load and render pending blocks
|
|
230
|
-
(function waitForMermaid() {
|
|
231
|
-
if (typeof mermaid !== 'undefined') {
|
|
232
|
-
ensureMarked(); // ensure mermaid.initialize() runs
|
|
233
|
-
renderMermaidBlocks();
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
setTimeout(waitForMermaid, 300);
|
|
237
|
-
})();
|
|
238
|
-
|
|
239
166
|
// ── Copy button event delegation (one-time setup) ──
|
|
240
167
|
let copyDelegationReady = false;
|
|
241
168
|
|
|
242
|
-
function ensureCopyDelegation() {
|
|
169
|
+
function ensureCopyDelegation(): void {
|
|
243
170
|
if (copyDelegationReady) return;
|
|
244
171
|
copyDelegationReady = true;
|
|
245
|
-
document.addEventListener('click', (e) => {
|
|
246
|
-
const label = e.target
|
|
172
|
+
document.addEventListener('click', (e: MouseEvent) => {
|
|
173
|
+
const label = (e.target as HTMLElement)?.closest('.code-lang-label') as HTMLElement | null;
|
|
247
174
|
if (!label) return;
|
|
248
175
|
const wrapper = label.closest('.code-block-wrapper');
|
|
249
176
|
if (!wrapper) return;
|
|
250
177
|
const codeEl = wrapper.querySelector('pre code');
|
|
251
178
|
if (!codeEl) return;
|
|
252
|
-
navigator.clipboard.writeText(codeEl.textContent).then(() => {
|
|
253
|
-
const orig = label.textContent;
|
|
179
|
+
navigator.clipboard.writeText(codeEl.textContent || '').then(() => {
|
|
180
|
+
const orig = label.textContent || '';
|
|
254
181
|
label.textContent = t('code.copied');
|
|
255
182
|
label.classList.add('copied');
|
|
256
183
|
setTimeout(() => {
|
|
@@ -262,13 +189,13 @@ function ensureCopyDelegation() {
|
|
|
262
189
|
}
|
|
263
190
|
|
|
264
191
|
// ── Main export ──
|
|
265
|
-
export function renderMarkdown(text) {
|
|
192
|
+
export function renderMarkdown(text: string): string {
|
|
266
193
|
const cleaned = stripOrchestration(text);
|
|
267
|
-
if (!cleaned) return `<em style="color:var(--text-dim)">${t('orchestrator.dispatching')}</em>`;
|
|
194
|
+
if (!cleaned) return `<em style="color:var(--text-dim)">${escapeHtml(t('orchestrator.dispatching'))}</em>`;
|
|
268
195
|
|
|
269
|
-
let html;
|
|
196
|
+
let html: string;
|
|
270
197
|
if (ensureMarked()) {
|
|
271
|
-
html = marked.parse(cleaned);
|
|
198
|
+
html = marked.parse(cleaned) as string;
|
|
272
199
|
// Wrap tables for horizontal scrolling
|
|
273
200
|
html = html.replace(/<table/g, '<div class="table-wrapper"><table').replace(/<\/table>/g, '</table></div>');
|
|
274
201
|
} else {
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// ── Shared State Module ──
|
|
2
|
+
// All modules import this to access/modify shared state.
|
|
3
|
+
// Object reference ensures mutations are seen across modules.
|
|
4
|
+
|
|
5
|
+
export interface HeartbeatJob {
|
|
6
|
+
id: string;
|
|
7
|
+
[key: string]: unknown;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface CliStatusCache {
|
|
11
|
+
[cli: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface AppState {
|
|
15
|
+
ws: WebSocket | null;
|
|
16
|
+
agentBusy: boolean;
|
|
17
|
+
employees: unknown[];
|
|
18
|
+
allSkills: unknown[];
|
|
19
|
+
currentSkillFilter: string;
|
|
20
|
+
currentAgentDiv: HTMLElement | null;
|
|
21
|
+
attachedFiles: File[];
|
|
22
|
+
heartbeatJobs: HeartbeatJob[];
|
|
23
|
+
cliStatusCache: CliStatusCache | null;
|
|
24
|
+
cliStatusTs: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const state: AppState = {
|
|
28
|
+
ws: null,
|
|
29
|
+
agentBusy: false,
|
|
30
|
+
employees: [],
|
|
31
|
+
allSkills: [],
|
|
32
|
+
currentSkillFilter: 'all',
|
|
33
|
+
currentAgentDiv: null,
|
|
34
|
+
attachedFiles: [],
|
|
35
|
+
heartbeatJobs: [],
|
|
36
|
+
cliStatusCache: null,
|
|
37
|
+
cliStatusTs: 0,
|
|
38
|
+
};
|
|
@@ -5,43 +5,42 @@ import { getAppName } from './features/appname.js';
|
|
|
5
5
|
import { t } from './features/i18n.js';
|
|
6
6
|
import { api } from './api.js';
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
interface ToolLogEntry { icon: string; label: string; }
|
|
9
|
+
interface MessageItem { role: string; content: string; }
|
|
10
|
+
interface MemoryItem { key: string; value: string; }
|
|
11
|
+
|
|
12
|
+
export function setStatus(s: string): void {
|
|
9
13
|
const badge = document.getElementById('statusBadge');
|
|
10
14
|
const btn = document.getElementById('btnSend');
|
|
11
15
|
state.agentBusy = s === 'running';
|
|
12
|
-
document.getElementById('typingIndicator')
|
|
16
|
+
document.getElementById('typingIndicator')?.classList.toggle('active', state.agentBusy);
|
|
13
17
|
if (s === 'running') {
|
|
14
|
-
badge.className = 'status-badge status-running';
|
|
15
|
-
|
|
16
|
-
btn.textContent = '■';
|
|
17
|
-
btn.title = t('btn.stop');
|
|
18
|
-
btn.classList.add('stop-mode');
|
|
18
|
+
if (badge) { badge.className = 'status-badge status-running'; badge.textContent = '⏳ running'; }
|
|
19
|
+
if (btn) { btn.textContent = '■'; btn.title = t('btn.stop'); btn.classList.add('stop-mode'); }
|
|
19
20
|
} else {
|
|
20
|
-
badge.className = 'status-badge status-idle';
|
|
21
|
-
|
|
22
|
-
btn.textContent = '➤';
|
|
23
|
-
btn.title = 'Send';
|
|
24
|
-
btn.classList.remove('stop-mode');
|
|
21
|
+
if (badge) { badge.className = 'status-badge status-idle'; badge.textContent = '⚡ idle'; }
|
|
22
|
+
if (btn) { btn.textContent = '➤'; btn.title = 'Send'; btn.classList.remove('stop-mode'); }
|
|
25
23
|
updateQueueBadge(0);
|
|
26
24
|
}
|
|
27
25
|
}
|
|
28
26
|
|
|
29
|
-
export function updateQueueBadge(count) {
|
|
27
|
+
export function updateQueueBadge(count: number): void {
|
|
30
28
|
let el = document.getElementById('queueBadge');
|
|
31
29
|
if (!el) {
|
|
32
30
|
el = document.createElement('span');
|
|
33
31
|
el.id = 'queueBadge';
|
|
34
32
|
el.style.cssText = 'position:absolute;top:-6px;right:-6px;background:#f80;color:#fff;border-radius:50%;font-size:11px;min-width:18px;height:18px;display:flex;align-items:center;justify-content:center;font-weight:bold';
|
|
35
|
-
document.getElementById('btnSend')
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
const sendBtn = document.getElementById('btnSend');
|
|
34
|
+
if (sendBtn?.parentElement) sendBtn.parentElement.style.position = 'relative';
|
|
35
|
+
if (sendBtn) { sendBtn.style.position = 'relative'; sendBtn.appendChild(el); }
|
|
38
36
|
}
|
|
39
|
-
el.textContent = count > 0 ? count : '';
|
|
37
|
+
el.textContent = count > 0 ? String(count) : '';
|
|
40
38
|
el.style.display = count > 0 ? 'flex' : 'none';
|
|
41
39
|
}
|
|
42
40
|
|
|
43
|
-
export function addSystemMsg(text, extraClass, type) {
|
|
41
|
+
export function addSystemMsg(text: string, extraClass?: string, type?: string): void {
|
|
44
42
|
const container = document.getElementById('chatMessages');
|
|
43
|
+
if (!container) return;
|
|
45
44
|
const div = document.createElement('div');
|
|
46
45
|
const typeClass = type ? ` msg-type-${type}` : '';
|
|
47
46
|
div.className = 'msg msg-system' + typeClass + (extraClass ? ' ' + extraClass : '');
|
|
@@ -50,19 +49,19 @@ export function addSystemMsg(text, extraClass, type) {
|
|
|
50
49
|
container.scrollTop = container.scrollHeight;
|
|
51
50
|
}
|
|
52
51
|
|
|
53
|
-
export function appendAgentText(text) {
|
|
52
|
+
export function appendAgentText(text: string): void {
|
|
54
53
|
if (!text) return;
|
|
55
54
|
if (!state.currentAgentDiv) {
|
|
56
55
|
state.currentAgentDiv = addMessage('agent', '');
|
|
57
56
|
}
|
|
58
|
-
const content = state.currentAgentDiv
|
|
59
|
-
content.textContent += text;
|
|
57
|
+
const content = (state.currentAgentDiv as HTMLElement)?.querySelector('.msg-content');
|
|
58
|
+
if (content) content.textContent += text;
|
|
60
59
|
scrollToBottom();
|
|
61
60
|
}
|
|
62
61
|
|
|
63
62
|
let lastFinalizeTs = 0;
|
|
64
63
|
|
|
65
|
-
export function finalizeAgent(text, toolLog) {
|
|
64
|
+
export function finalizeAgent(text: string, toolLog?: ToolLogEntry[]): void {
|
|
66
65
|
// Guard: prevent double-render when both agent_done + orchestrate_done fire
|
|
67
66
|
const now = Date.now();
|
|
68
67
|
if (!state.currentAgentDiv && now - lastFinalizeTs < 500) return;
|
|
@@ -72,17 +71,16 @@ export function finalizeAgent(text, toolLog) {
|
|
|
72
71
|
if (!state.currentAgentDiv) {
|
|
73
72
|
state.currentAgentDiv = addMessage('agent', '');
|
|
74
73
|
}
|
|
75
|
-
const content = state.currentAgentDiv
|
|
76
|
-
state.currentAgentDiv.dataset.rawText = text;
|
|
74
|
+
const content = (state.currentAgentDiv as HTMLElement)?.querySelector('.msg-content');
|
|
77
75
|
let toolHtml = '';
|
|
78
76
|
if (toolLog && toolLog.length > 0) {
|
|
79
|
-
const counts = {};
|
|
80
|
-
toolLog.forEach(
|
|
77
|
+
const counts: Record<string, number> = {};
|
|
78
|
+
toolLog.forEach(tl => { counts[tl.icon] = (counts[tl.icon] || 0) + 1; });
|
|
81
79
|
const summaryParts = Object.entries(counts).map(([icon, n]) => `${icon}×${n}`).join(' ');
|
|
82
|
-
const logLines = toolLog.map(
|
|
80
|
+
const logLines = toolLog.map(tl => `${tl.icon} ${escapeHtml(tl.label)}`).join('\n');
|
|
83
81
|
toolHtml = `<details class="tool-summary"><summary>${summaryParts}</summary><div class="tool-log">${logLines}</div></details>`;
|
|
84
82
|
}
|
|
85
|
-
content.innerHTML = toolHtml + renderMarkdown(text);
|
|
83
|
+
if (content) content.innerHTML = toolHtml + renderMarkdown(text);
|
|
86
84
|
}
|
|
87
85
|
state.currentAgentDiv = null;
|
|
88
86
|
lastFinalizeTs = Date.now();
|
|
@@ -90,29 +88,27 @@ export function finalizeAgent(text, toolLog) {
|
|
|
90
88
|
loadStats();
|
|
91
89
|
}
|
|
92
90
|
|
|
93
|
-
export function addMessage(role, text) {
|
|
91
|
+
export function addMessage(role: string, text: string): HTMLDivElement {
|
|
94
92
|
const container = document.getElementById('chatMessages');
|
|
95
93
|
const div = document.createElement('div');
|
|
96
94
|
div.className = `msg msg-${role}`;
|
|
97
|
-
div.dataset.rawText = text;
|
|
98
95
|
const rendered = renderMarkdown(text);
|
|
99
|
-
|
|
100
|
-
div
|
|
101
|
-
container.appendChild(div);
|
|
96
|
+
div.innerHTML = `<div class="msg-label">${role === 'user' ? t('msg.you') : getAppName()}</div><div class="msg-content">${rendered}</div>`;
|
|
97
|
+
container?.appendChild(div);
|
|
102
98
|
scrollToBottom();
|
|
103
99
|
return div;
|
|
104
100
|
}
|
|
105
101
|
|
|
106
|
-
export function scrollToBottom() {
|
|
102
|
+
export function scrollToBottom(): void {
|
|
107
103
|
const c = document.getElementById('chatMessages');
|
|
108
|
-
c.scrollTop = c.scrollHeight;
|
|
104
|
+
if (c) c.scrollTop = c.scrollHeight;
|
|
109
105
|
}
|
|
110
106
|
|
|
111
|
-
export function switchTab(name, targetBtn) {
|
|
107
|
+
export function switchTab(name: string, targetBtn: Element): void {
|
|
112
108
|
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('active'));
|
|
113
109
|
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
|
114
|
-
const tabMap = { agents: 'tabAgents', settings: 'tabSettings', skills: 'tabSkills' };
|
|
115
|
-
document.getElementById(tabMap[name])
|
|
110
|
+
const tabMap: Record<string, string> = { agents: 'tabAgents', settings: 'tabSettings', skills: 'tabSkills' };
|
|
111
|
+
document.getElementById(tabMap[name])?.classList.add('active');
|
|
116
112
|
if (targetBtn) targetBtn.classList.add('active');
|
|
117
113
|
// Lazy-load tab content
|
|
118
114
|
if (name === 'settings') { import('./features/settings.js').then(m => m.loadSettings()); }
|
|
@@ -120,8 +116,8 @@ export function switchTab(name, targetBtn) {
|
|
|
120
116
|
if (name === 'skills') { import('./features/skills.js').then(m => m.loadSkills()); }
|
|
121
117
|
}
|
|
122
118
|
|
|
123
|
-
export function handleSave() {
|
|
124
|
-
const isSettings = document.getElementById('tabSettings')
|
|
119
|
+
export function handleSave(): void {
|
|
120
|
+
const isSettings = document.getElementById('tabSettings')?.classList.contains('active');
|
|
125
121
|
if (isSettings) {
|
|
126
122
|
import('./features/settings.js').then(m => m.savePerCli());
|
|
127
123
|
} else {
|
|
@@ -129,38 +125,24 @@ export function handleSave() {
|
|
|
129
125
|
}
|
|
130
126
|
}
|
|
131
127
|
|
|
132
|
-
export async function loadStats() {
|
|
133
|
-
const msgs = await api('/api/messages');
|
|
128
|
+
export async function loadStats(): Promise<void> {
|
|
129
|
+
const msgs = await api<MessageItem[]>('/api/messages');
|
|
134
130
|
if (!msgs) return;
|
|
135
|
-
document.getElementById('statMsgs')
|
|
131
|
+
const el = document.getElementById('statMsgs');
|
|
132
|
+
if (el) el.textContent = t('stat.messages', { count: msgs.length });
|
|
136
133
|
}
|
|
137
134
|
|
|
138
|
-
export async function loadMessages() {
|
|
139
|
-
const msgs = await api('/api/messages');
|
|
135
|
+
export async function loadMessages(): Promise<void> {
|
|
136
|
+
const msgs = await api<MessageItem[]>('/api/messages');
|
|
140
137
|
if (!msgs) return;
|
|
141
138
|
msgs.forEach(m => addMessage(m.role === 'assistant' ? 'agent' : m.role, m.content));
|
|
142
139
|
}
|
|
143
140
|
|
|
144
|
-
|
|
145
|
-
export function initMsgCopy() {
|
|
146
|
-
document.getElementById('chatMessages').addEventListener('click', async (e) => {
|
|
147
|
-
const btn = e.target.closest('.msg-copy');
|
|
148
|
-
if (!btn) return;
|
|
149
|
-
const msg = btn.closest('.msg');
|
|
150
|
-
const raw = msg?.dataset?.rawText || msg?.querySelector('.msg-content')?.innerText || '';
|
|
151
|
-
try {
|
|
152
|
-
await navigator.clipboard.writeText(raw);
|
|
153
|
-
btn.classList.add('copied');
|
|
154
|
-
setTimeout(() => { btn.classList.remove('copied'); }, 1500);
|
|
155
|
-
} catch { setTimeout(() => { }, 1500); }
|
|
156
|
-
});
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
export async function loadMemory() {
|
|
141
|
+
export async function loadMemory(): Promise<void> {
|
|
160
142
|
try {
|
|
161
|
-
const items = await api('/api/memory');
|
|
143
|
+
const items = await api<MemoryItem[]>('/api/memory');
|
|
162
144
|
const list = document.getElementById('memoryList');
|
|
163
|
-
if (!list) return;
|
|
145
|
+
if (!list || !items) return;
|
|
164
146
|
if (items.length === 0) {
|
|
165
147
|
list.innerHTML = `<li style="color:var(--text-dim)">${t('mem.empty')}</li>`;
|
|
166
148
|
return;
|
|
@@ -170,3 +152,18 @@ export async function loadMemory() {
|
|
|
170
152
|
).join('');
|
|
171
153
|
} catch { }
|
|
172
154
|
}
|
|
155
|
+
|
|
156
|
+
// ── Message copy delegation ──
|
|
157
|
+
export function initMsgCopy(): void {
|
|
158
|
+
document.getElementById('chatMessages')?.addEventListener('click', (e) => {
|
|
159
|
+
const msgContent = (e.target as HTMLElement)?.closest('.msg-content');
|
|
160
|
+
if (!msgContent) return;
|
|
161
|
+
// Double-click to copy (not single click)
|
|
162
|
+
});
|
|
163
|
+
document.getElementById('chatMessages')?.addEventListener('dblclick', (e) => {
|
|
164
|
+
const msgContent = (e.target as HTMLElement)?.closest('.msg-content') as HTMLElement | null;
|
|
165
|
+
if (!msgContent) return;
|
|
166
|
+
const text = msgContent.innerText || msgContent.textContent || '';
|
|
167
|
+
navigator.clipboard.writeText(text).catch(() => { });
|
|
168
|
+
});
|
|
169
|
+
}
|
|
@@ -3,18 +3,42 @@ import { state } from './state.js';
|
|
|
3
3
|
import { setStatus, updateQueueBadge, addSystemMsg, appendAgentText, finalizeAgent, addMessage } from './ui.js';
|
|
4
4
|
import { t, getLang } from './features/i18n.js';
|
|
5
5
|
|
|
6
|
+
interface WsMessage {
|
|
7
|
+
type: string;
|
|
8
|
+
running?: boolean;
|
|
9
|
+
status?: string;
|
|
10
|
+
agentId?: string;
|
|
11
|
+
phase?: string;
|
|
12
|
+
phaseLabel?: string;
|
|
13
|
+
pending?: number;
|
|
14
|
+
path?: string;
|
|
15
|
+
round?: number;
|
|
16
|
+
agentPhases?: { agent?: string; name?: string }[];
|
|
17
|
+
subtasks?: { agent?: string; name?: string }[];
|
|
18
|
+
action?: string;
|
|
19
|
+
icon?: string;
|
|
20
|
+
label?: string;
|
|
21
|
+
text?: string;
|
|
22
|
+
toolLog?: { icon: string; label: string }[];
|
|
23
|
+
from?: string;
|
|
24
|
+
to?: string;
|
|
25
|
+
source?: string;
|
|
26
|
+
role?: string;
|
|
27
|
+
content?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
6
30
|
// Agent phase state (populated by agent_status events from orchestrator)
|
|
7
|
-
const agentPhaseState = {};
|
|
31
|
+
const agentPhaseState: Record<string, { phase: string; phaseLabel: string }> = {};
|
|
8
32
|
|
|
9
|
-
export function connect() {
|
|
33
|
+
export function connect(): void {
|
|
10
34
|
state.ws = new WebSocket(`ws://${location.host}?lang=${getLang()}`);
|
|
11
|
-
state.ws.onmessage = (e) => {
|
|
12
|
-
const msg = JSON.parse(e.data);
|
|
35
|
+
state.ws.onmessage = (e: MessageEvent) => {
|
|
36
|
+
const msg: WsMessage = JSON.parse(e.data as string);
|
|
13
37
|
if (msg.type === 'agent_status') {
|
|
14
38
|
if (msg.running !== undefined) {
|
|
15
39
|
setStatus(msg.running ? 'running' : 'idle');
|
|
16
40
|
} else {
|
|
17
|
-
setStatus(msg.status);
|
|
41
|
+
setStatus(msg.status || 'idle');
|
|
18
42
|
}
|
|
19
43
|
// Track per-agent phase for badge rendering
|
|
20
44
|
if (msg.agentId && msg.phase) {
|
|
@@ -24,42 +48,44 @@ export function connect() {
|
|
|
24
48
|
} else if (msg.type === 'queue_update') {
|
|
25
49
|
updateQueueBadge(msg.pending || 0);
|
|
26
50
|
} else if (msg.type === 'worklog_created') {
|
|
27
|
-
addSystemMsg(`📋 Worklog: ${msg.path}`);
|
|
51
|
+
addSystemMsg(`📋 Worklog: ${msg.path || ''}`);
|
|
28
52
|
} else if (msg.type === 'round_start') {
|
|
29
53
|
const agents = (msg.agentPhases || msg.subtasks || []);
|
|
30
|
-
const names = agents.map(a => a.agent || a.name).join(', ');
|
|
31
|
-
addSystemMsg(t('ws.roundStart', { round: msg.round, count: agents.length, names }));
|
|
54
|
+
const names = agents.map(a => a.agent || a.name || '').join(', ');
|
|
55
|
+
addSystemMsg(t('ws.roundStart', { round: msg.round || 0, count: agents.length, names }));
|
|
32
56
|
} else if (msg.type === 'round_done') {
|
|
33
57
|
if (msg.action === 'complete') {
|
|
34
|
-
addSystemMsg(t('ws.roundDone', { round: msg.round }));
|
|
58
|
+
addSystemMsg(t('ws.roundDone', { round: msg.round || 0 }));
|
|
35
59
|
} else if (msg.action === 'next') {
|
|
36
|
-
addSystemMsg(t('ws.roundNext', { round: msg.round }));
|
|
60
|
+
addSystemMsg(t('ws.roundNext', { round: msg.round || 0 }));
|
|
37
61
|
} else {
|
|
38
|
-
addSystemMsg(t('ws.roundRetry', { round: msg.round }));
|
|
62
|
+
addSystemMsg(t('ws.roundRetry', { round: msg.round || 0 }));
|
|
39
63
|
}
|
|
40
64
|
} else if (msg.type === 'agent_tool') {
|
|
41
|
-
addSystemMsg(`${msg.icon} ${msg.label}`, 'tool-activity');
|
|
65
|
+
addSystemMsg(`${msg.icon || ''} ${msg.label || ''}`, 'tool-activity');
|
|
42
66
|
} else if (msg.type === 'agent_output') {
|
|
43
|
-
appendAgentText(msg.text);
|
|
67
|
+
appendAgentText(msg.text || '');
|
|
44
68
|
} else if (msg.type === 'agent_fallback') {
|
|
45
|
-
addSystemMsg(t('ws.fallback', { from: msg.from, to: msg.to }), 'tool-activity');
|
|
69
|
+
addSystemMsg(t('ws.fallback', { from: msg.from || '', to: msg.to || '' }), 'tool-activity');
|
|
46
70
|
} else if (msg.type === 'agent_done') {
|
|
47
|
-
finalizeAgent(msg.text, msg.toolLog);
|
|
71
|
+
finalizeAgent(msg.text || '', msg.toolLog);
|
|
48
72
|
} else if (msg.type === 'orchestrate_done') {
|
|
49
|
-
finalizeAgent(msg.text);
|
|
73
|
+
finalizeAgent(msg.text || '');
|
|
50
74
|
} else if (msg.type === 'clear') {
|
|
51
|
-
document.getElementById('chatMessages')
|
|
75
|
+
const el = document.getElementById('chatMessages');
|
|
76
|
+
if (el) el.innerHTML = '';
|
|
52
77
|
} else if (msg.type === 'agent_added' || msg.type === 'agent_updated' || msg.type === 'agent_deleted') {
|
|
53
78
|
import('./features/employees.js').then(m => m.loadEmployees());
|
|
54
79
|
} else if (msg.type === 'new_message' && msg.source === 'telegram') {
|
|
55
|
-
addMessage(msg.role === 'assistant' ? 'agent' : msg.role, msg.content);
|
|
80
|
+
addMessage(msg.role === 'assistant' ? 'agent' : (msg.role || 'user'), msg.content || '');
|
|
56
81
|
}
|
|
57
82
|
};
|
|
58
83
|
state.ws.onopen = () => {
|
|
59
84
|
console.log('[ws] connected');
|
|
60
85
|
// Restore state: reload messages to stay in sync after reconnect
|
|
61
86
|
import('./ui.js').then(m => {
|
|
62
|
-
document.getElementById('chatMessages')
|
|
87
|
+
const el = document.getElementById('chatMessages');
|
|
88
|
+
if (el) el.innerHTML = '';
|
|
63
89
|
m.loadMessages();
|
|
64
90
|
m.setStatus('idle');
|
|
65
91
|
});
|
|
@@ -71,6 +97,6 @@ export function connect() {
|
|
|
71
97
|
};
|
|
72
98
|
}
|
|
73
99
|
|
|
74
|
-
export function getAgentPhase(agentId) {
|
|
100
|
+
export function getAgentPhase(agentId: string): { phase: string; phaseLabel: string } | null {
|
|
75
101
|
return agentPhaseState[agentId] || null;
|
|
76
102
|
}
|
package/public/locales/en.json
CHANGED