cli-jaw 1.5.0 → 1.6.2
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 +26 -19
- package/README.md +50 -22
- package/README.zh-CN.md +26 -19
- package/dist/bin/cli-jaw.js +5 -1
- package/dist/bin/cli-jaw.js.map +1 -1
- package/dist/bin/commands/dispatch.js +57 -0
- package/dist/bin/commands/dispatch.js.map +1 -0
- package/dist/lib/mcp-sync.js +1 -1
- package/dist/lib/mcp-sync.js.map +1 -1
- package/dist/lib/upload.js +14 -1
- package/dist/lib/upload.js.map +1 -1
- package/dist/server.js +57 -42
- package/dist/server.js.map +1 -1
- package/dist/src/agent/events.js +72 -13
- package/dist/src/agent/events.js.map +1 -1
- package/dist/src/agent/spawn.js +71 -29
- package/dist/src/agent/spawn.js.map +1 -1
- package/dist/src/cli/acp-client.js +4 -2
- package/dist/src/cli/acp-client.js.map +1 -1
- package/dist/src/cli/commands.js +6 -6
- package/dist/src/cli/commands.js.map +1 -1
- package/dist/src/cli/handlers.js +11 -9
- package/dist/src/cli/handlers.js.map +1 -1
- package/dist/src/cli/registry.js +7 -1
- package/dist/src/cli/registry.js.map +1 -1
- package/dist/src/core/config.js +15 -8
- package/dist/src/core/config.js.map +1 -1
- package/dist/src/core/db.js +18 -3
- package/dist/src/core/db.js.map +1 -1
- package/dist/src/core/employees.js +34 -0
- package/dist/src/core/employees.js.map +1 -0
- package/dist/src/discord/bot.js +72 -9
- package/dist/src/discord/bot.js.map +1 -1
- package/dist/src/discord/commands.js +12 -8
- package/dist/src/discord/commands.js.map +1 -1
- package/dist/src/orchestrator/distribute.js +57 -35
- package/dist/src/orchestrator/distribute.js.map +1 -1
- package/dist/src/orchestrator/gateway.js +3 -1
- package/dist/src/orchestrator/gateway.js.map +1 -1
- package/dist/src/orchestrator/pipeline.js +120 -27
- package/dist/src/orchestrator/pipeline.js.map +1 -1
- package/dist/src/orchestrator/research.js +5 -5
- package/dist/src/orchestrator/research.js.map +1 -1
- package/dist/src/orchestrator/scope.js +55 -0
- package/dist/src/orchestrator/scope.js.map +1 -0
- package/dist/src/orchestrator/state-machine.js +23 -21
- package/dist/src/orchestrator/state-machine.js.map +1 -1
- package/dist/src/orchestrator/worker-registry.js +9 -2
- package/dist/src/orchestrator/worker-registry.js.map +1 -1
- package/dist/src/prompt/builder.js +76 -37
- package/dist/src/prompt/builder.js.map +1 -1
- package/dist/src/prompt/templates/a1-system.md +40 -0
- package/dist/src/prompt/templates/employee.md +12 -4
- package/dist/src/prompt/templates/orchestration.md +17 -1
- package/dist/src/telegram/bot.js +7 -1
- package/dist/src/telegram/bot.js.map +1 -1
- package/package.json +4 -2
- package/public/assets/fonts/GeistVF.woff2 +0 -0
- package/public/assets/fonts/JetBrainsMono-Variable.woff2 +0 -0
- package/public/assets/providers/claude-color.svg +1 -0
- package/public/assets/providers/claude.svg +1 -0
- package/public/assets/providers/copilot-color.svg +1 -0
- package/public/assets/providers/copilot.svg +1 -0
- package/public/assets/providers/discord.svg +1 -0
- package/public/assets/providers/gemini-color.svg +1 -0
- package/public/assets/providers/gemini.svg +1 -0
- package/public/assets/providers/openai.svg +1 -0
- package/public/assets/providers/opencode.svg +1 -0
- package/public/assets/providers/telegram.svg +1 -0
- package/public/css/chat.css +79 -51
- package/public/css/diagram.css +348 -0
- package/public/css/layout.css +38 -11
- package/public/css/markdown.css +64 -18
- package/public/css/modals.css +3 -3
- package/public/css/orc-state.css +9 -9
- package/public/css/sidebar.css +37 -3
- package/public/css/tool-ui.css +46 -11
- package/public/css/variables.css +63 -63
- package/public/dist/assets/api-Bbmmo0o1.js +1 -0
- package/public/dist/assets/architecture-YZFGNWBL-BxiHqtOH.js +1 -0
- package/public/dist/assets/architectureDiagram-Q4EWVU46-CJTGDw5p.js +1 -0
- package/public/dist/assets/blockDiagram-DXYQGD6D-Btl5Y-Er.js +1 -0
- package/public/dist/assets/c4Diagram-AHTNJAMY-sDfjW4ZF.js +1 -0
- package/public/dist/assets/classDiagram-6PBFFD2Q-ByCgggL2.js +1 -0
- package/public/dist/assets/classDiagram-v2-HSJHXN6E-t1amaXkU.js +1 -0
- package/public/dist/assets/constants-C8_0OtK2.js +1 -0
- package/public/dist/assets/cose-bilkent-S5V4N54A-eOSFGdaE.js +1 -0
- package/public/dist/assets/dagre-KV5264BT-OCB5j8Q6.js +1 -0
- package/public/dist/assets/diagram-5BDNPKRD-C-4fgK5P.js +1 -0
- package/public/dist/assets/diagram-G4DWMVQ6-C6vmHKDV.js +1 -0
- package/public/dist/assets/diagram-MMDJMWI5-BMCo_wmt.js +1 -0
- package/public/dist/assets/diagram-TYMM5635-DaCLdttJ.js +1 -0
- package/public/dist/assets/employees-D5n7mX5v.js +39 -0
- package/public/dist/assets/erDiagram-SMLLAGMA-BiTRdm5s.js +1 -0
- package/public/dist/assets/flowDiagram-DWJPFMVM-sv6jVi0d.js +1 -0
- package/public/dist/assets/ganttDiagram-T4ZO3ILL-jCP3OW23.js +1 -0
- package/public/dist/assets/gitGraph-7Q5UKJZL-BIeKLxpm.js +1 -0
- package/public/dist/assets/gitGraphDiagram-UUTBAWPF-CokylnbW.js +1 -0
- package/public/dist/assets/index-CLd0BsAu.js +49 -0
- package/public/dist/assets/index-D6ci1wCN.css +1 -0
- package/public/dist/assets/info-OMHHGYJF-CIEl5dWF.js +1 -0
- package/public/dist/assets/infoDiagram-42DDH7IO-BBnK1Bh2.js +1 -0
- package/public/dist/assets/ishikawaDiagram-UXIWVN3A-qXIz0VhS.js +1 -0
- package/public/dist/assets/journeyDiagram-VCZTEJTY-BdrOLof3.js +1 -0
- package/public/dist/assets/kanban-definition-6JOO6SKY-CcX7CE04.js +1 -0
- package/public/dist/assets/mermaid.core-BoxIvw7E.js +1 -0
- package/public/dist/assets/mindmap-definition-QFDTVHPH-CdXARskX.js +1 -0
- package/public/dist/assets/packet-4T2RLAQJ-Bz4ZwYZ3.js +1 -0
- package/public/dist/assets/pie-ZZUOXDRM-AtGL3wZ2.js +1 -0
- package/public/dist/assets/pieDiagram-DEJITSTG-B5SOq9Yr.js +1 -0
- package/public/dist/assets/quadrantDiagram-34T5L4WZ-D0uNWgpI.js +1 -0
- package/public/dist/assets/radar-PYXPWWZC-AbJSfeqB.js +1 -0
- package/public/dist/assets/render-C8N0rp4L.js +25 -0
- package/public/dist/assets/requirementDiagram-MS252O5E-62td6IQ-.js +1 -0
- package/public/dist/assets/sankeyDiagram-XADWPNL6-Crg_02iC.js +1 -0
- package/public/dist/assets/sequenceDiagram-FGHM5R23-fbCocZHj.js +1 -0
- package/public/dist/assets/settings-BJR-IGM5.js +41 -0
- package/public/dist/assets/settings-Dm6OnPmY.js +1 -0
- package/public/dist/assets/skills-D6jv9AIs.js +1 -0
- package/public/dist/assets/skills-uywskdFh.js +12 -0
- package/public/dist/assets/slash-commands-CCDonT40.js +1 -0
- package/public/dist/assets/{slash-commands-DMsE88bu.js → slash-commands-DWvL-VDU.js} +1 -1
- package/public/dist/assets/stateDiagram-FHFEXIEX-Gy3DhXxQ.js +1 -0
- package/public/dist/assets/stateDiagram-v2-QKLJ7IA2-DJc-FW9O.js +1 -0
- package/public/dist/assets/timeline-definition-GMOUNBTQ-B3cA9DgY.js +1 -0
- package/public/dist/assets/treeView-SZITEDCU-Cn58DIbB.js +1 -0
- package/public/dist/assets/treemap-W4RFUUIX-DPcYjDzH.js +1 -0
- package/public/dist/assets/ui-C1daR00l.js +1 -0
- package/public/dist/assets/ui-D-oFkXed.js +131 -0
- package/public/dist/assets/vendor-icons-1Ec7ZWcT.js +1 -0
- package/public/dist/assets/{vendor-mermaid-COidH9HB.js → vendor-mermaid-lvHqQdfg.js} +4 -4
- package/public/dist/assets/vennDiagram-DHZGUBPP-DkBfxilo.js +1 -0
- package/public/dist/assets/wardley-RL74JXVD-6Dg0PJ4H.js +1 -0
- package/public/dist/assets/wardleyDiagram-NUSXRM2D-Dsntl_vy.js +1 -0
- package/public/dist/assets/ws-Dnn8HG8B.js +2 -0
- package/public/dist/assets/xychartDiagram-5P7HB3ND-B_McB5GE.js +1 -0
- package/public/dist/index.html +63 -55
- package/public/index.html +62 -53
- package/public/js/constants.ts +8 -2
- package/public/js/diagram/iframe-renderer.ts +559 -0
- package/public/js/diagram/types.ts +129 -0
- package/public/js/diagram/widget-validator.ts +82 -0
- package/public/js/features/chat.ts +24 -12
- package/public/js/features/employees.ts +3 -2
- package/public/js/features/heartbeat.ts +4 -3
- package/public/js/features/memory.ts +4 -3
- package/public/js/features/process-block.ts +12 -11
- package/public/js/features/settings-cli-status.ts +10 -7
- package/public/js/features/settings-core.ts +13 -3
- package/public/js/features/settings-discord.ts +9 -0
- package/public/js/features/settings-mcp.ts +8 -7
- package/public/js/features/settings-stt.ts +4 -4
- package/public/js/features/settings-templates.ts +10 -8
- package/public/js/features/settings-types.ts +1 -1
- package/public/js/features/settings.ts +1 -1
- package/public/js/features/sidebar.ts +37 -7
- package/public/js/features/skills.ts +4 -3
- package/public/js/features/theme.ts +4 -0
- package/public/js/features/tool-ui.ts +6 -5
- package/public/js/features/voice-recorder.ts +2 -1
- package/public/js/icons.ts +257 -0
- package/public/js/main.ts +9 -3
- package/public/js/provider-icons.ts +88 -0
- package/public/js/render.ts +493 -30
- package/public/js/streaming-render.ts +3 -3
- package/public/js/ui.ts +38 -18
- package/public/js/ws.ts +17 -10
- package/public/locales/en.json +89 -88
- package/public/locales/ko.json +89 -88
- package/scripts/release-1.6.0.sh +69 -0
- package/scripts/release-preview.sh +1 -1
- package/dist/lib/token-keepalive.js +0 -34
- package/dist/lib/token-keepalive.js.map +0 -1
- package/public/dist/assets/api-BlPw3bUI.js +0 -1
- package/public/dist/assets/architecture-YZFGNWBL-BkS7SZQi.js +0 -1
- package/public/dist/assets/architectureDiagram-Q4EWVU46-Dl3iBKeR.js +0 -1
- package/public/dist/assets/blockDiagram-DXYQGD6D-DPr4D17p.js +0 -1
- package/public/dist/assets/c4Diagram-AHTNJAMY-CfoxJtDk.js +0 -1
- package/public/dist/assets/classDiagram-6PBFFD2Q-BOAdHnnB.js +0 -1
- package/public/dist/assets/classDiagram-v2-HSJHXN6E-B2QCJXWC.js +0 -1
- package/public/dist/assets/constants-DshMUJbo.js +0 -1
- package/public/dist/assets/cose-bilkent-S5V4N54A-CA14jk7w.js +0 -1
- package/public/dist/assets/dagre-KV5264BT-BVwNaSwC.js +0 -1
- package/public/dist/assets/diagram-5BDNPKRD-CHqIAvdc.js +0 -1
- package/public/dist/assets/diagram-G4DWMVQ6-DxvsCvTP.js +0 -1
- package/public/dist/assets/diagram-MMDJMWI5-DqOPO7dl.js +0 -1
- package/public/dist/assets/diagram-TYMM5635-C9xMWPQn.js +0 -1
- package/public/dist/assets/employees-CFRlsbHm.js +0 -39
- package/public/dist/assets/erDiagram-SMLLAGMA-BTmpfRkm.js +0 -1
- package/public/dist/assets/flowDiagram-DWJPFMVM-DZBSQAKA.js +0 -1
- package/public/dist/assets/ganttDiagram-T4ZO3ILL-TAEMCRbT.js +0 -1
- package/public/dist/assets/gitGraph-7Q5UKJZL-bbxndRNA.js +0 -1
- package/public/dist/assets/gitGraphDiagram-UUTBAWPF-CBxtx0MB.js +0 -1
- package/public/dist/assets/index-1Gg-6jeC.css +0 -1
- package/public/dist/assets/index-CQHqXjrK.js +0 -49
- package/public/dist/assets/info-OMHHGYJF-DRXq_Ywr.js +0 -1
- package/public/dist/assets/infoDiagram-42DDH7IO-C7FFwX4m.js +0 -1
- package/public/dist/assets/ishikawaDiagram-UXIWVN3A-DEZJE79j.js +0 -1
- package/public/dist/assets/journeyDiagram-VCZTEJTY-ZHLiY6GV.js +0 -1
- package/public/dist/assets/kanban-definition-6JOO6SKY-NP7pY7ML.js +0 -1
- package/public/dist/assets/mermaid.core-CHuluSlD.js +0 -1
- package/public/dist/assets/mindmap-definition-QFDTVHPH-Bd1OgfN_.js +0 -1
- package/public/dist/assets/packet-4T2RLAQJ-CGu8ST97.js +0 -1
- package/public/dist/assets/pie-ZZUOXDRM-CDG65mhS.js +0 -1
- package/public/dist/assets/pieDiagram-DEJITSTG-BbMBHLDM.js +0 -1
- package/public/dist/assets/quadrantDiagram-34T5L4WZ-CDCDgI1e.js +0 -1
- package/public/dist/assets/radar-PYXPWWZC-1gS-6ylu.js +0 -1
- package/public/dist/assets/render-YHvL5VM_.js +0 -6
- package/public/dist/assets/requirementDiagram-MS252O5E-DkmvOtYz.js +0 -1
- package/public/dist/assets/sankeyDiagram-XADWPNL6-GNcXIYsL.js +0 -1
- package/public/dist/assets/sequenceDiagram-FGHM5R23-BFrw2n6x.js +0 -1
- package/public/dist/assets/settings-BQF_u5px.js +0 -37
- package/public/dist/assets/settings-C5Q9IPjJ.js +0 -1
- package/public/dist/assets/skills-9Pd2rOw-.js +0 -1
- package/public/dist/assets/skills-BI7sQAdk.js +0 -12
- package/public/dist/assets/slash-commands-FMpJzlDB.js +0 -1
- package/public/dist/assets/stateDiagram-FHFEXIEX-BLgKnllT.js +0 -1
- package/public/dist/assets/stateDiagram-v2-QKLJ7IA2-CbzuWsSa.js +0 -1
- package/public/dist/assets/timeline-definition-GMOUNBTQ-CcZTJ3gL.js +0 -1
- package/public/dist/assets/treeView-SZITEDCU-BDqBsaH1.js +0 -1
- package/public/dist/assets/treemap-W4RFUUIX-BaWHA3Di.js +0 -1
- package/public/dist/assets/ui-BukgLHuh.js +0 -29
- package/public/dist/assets/ui-Bvz1JfTE.js +0 -1
- package/public/dist/assets/vennDiagram-DHZGUBPP-BWV_j1bh.js +0 -1
- package/public/dist/assets/wardley-RL74JXVD-C7gKA3d7.js +0 -1
- package/public/dist/assets/wardleyDiagram-NUSXRM2D-BUoq_wug.js +0 -1
- package/public/dist/assets/ws-S_AZgx7L.js +0 -2
- package/public/dist/assets/xychartDiagram-5P7HB3ND-PvT5Ec82.js +0 -1
- /package/public/dist/assets/{api-B8XKQ4OT.js → api-Ci-lgwRp.js} +0 -0
- /package/public/dist/assets/{locale-BjoAcbis.js → locale-DIXc-_34.js} +0 -0
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// ── Widget HTML Validator ──
|
|
2
|
+
// Client-side validation for AI-generated diagram-html before iframe injection.
|
|
3
|
+
// Defense-in-depth layer on top of sandbox="allow-scripts" + CSP.
|
|
4
|
+
|
|
5
|
+
const CDN_ALLOWLIST = [
|
|
6
|
+
'cdnjs.cloudflare.com',
|
|
7
|
+
'cdn.jsdelivr.net',
|
|
8
|
+
'unpkg.com',
|
|
9
|
+
'esm.sh',
|
|
10
|
+
'fonts.googleapis.com',
|
|
11
|
+
'fonts.gstatic.com',
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const DANGEROUS_PATTERNS: readonly RegExp[] = [
|
|
15
|
+
/\beval\s*\(/,
|
|
16
|
+
/\bnew\s+Function\s*\(/,
|
|
17
|
+
/\bdocument\.cookie\b/,
|
|
18
|
+
/\bwindow\.opener\b/,
|
|
19
|
+
/\bwindow\.top\b/,
|
|
20
|
+
/\bparent\.postMessage\b(?!.*jaw-)/,
|
|
21
|
+
/\blocation\.href\s*=/,
|
|
22
|
+
/\bwindow\.location\b/,
|
|
23
|
+
/\bsetTimeout\s*\(\s*["'`]/,
|
|
24
|
+
/\bsetInterval\s*\(\s*["'`]/,
|
|
25
|
+
/\.constructor\s*\.\s*constructor/,
|
|
26
|
+
/\bdocument\.write\s*\(/,
|
|
27
|
+
/\binsertAdjacentHTML\s*\(/,
|
|
28
|
+
/\bimport\s*\(/,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
// No warn-only patterns currently — innerHTML removed (too common in onerror fallbacks)
|
|
32
|
+
const WARN_PATTERNS: readonly RegExp[] = [];
|
|
33
|
+
|
|
34
|
+
export interface ValidationResult {
|
|
35
|
+
valid: boolean;
|
|
36
|
+
reason?: string;
|
|
37
|
+
warnings: string[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function validateWidgetHtml(html: string): ValidationResult {
|
|
41
|
+
const warnings: string[] = [];
|
|
42
|
+
|
|
43
|
+
// 1. Size cap (redundant with activateWidgets but defense-in-depth)
|
|
44
|
+
if (html.length > 524_288) {
|
|
45
|
+
return { valid: false, reason: 'Payload too large (>512KB)', warnings };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 2. External URL validation — only allowlisted CDN domains
|
|
49
|
+
const urlPattern = /(?:src|href)\s*=\s*["']https?:\/\/([^/"']+)/gi;
|
|
50
|
+
let match;
|
|
51
|
+
while ((match = urlPattern.exec(html)) !== null) {
|
|
52
|
+
const domain = match[1];
|
|
53
|
+
if (!CDN_ALLOWLIST.some(a => domain === a || domain.endsWith('.' + a))) {
|
|
54
|
+
return { valid: false, reason: `Blocked domain: ${domain}`, warnings };
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 3. CSS url() with external resources
|
|
59
|
+
const cssUrlPattern = /url\s*\(\s*['"]?https?:\/\/([^)'"]+)/gi;
|
|
60
|
+
while ((match = cssUrlPattern.exec(html)) !== null) {
|
|
61
|
+
const domain = match[1].split('/')[0];
|
|
62
|
+
if (!CDN_ALLOWLIST.some(a => domain === a || domain.endsWith('.' + a))) {
|
|
63
|
+
warnings.push(`CSS url() references external domain: ${domain}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 4. Dangerous patterns — block
|
|
68
|
+
for (const pattern of DANGEROUS_PATTERNS) {
|
|
69
|
+
if (pattern.test(html)) {
|
|
70
|
+
return { valid: false, reason: `Dangerous pattern: ${pattern.source}`, warnings };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// 5. Warning-only patterns
|
|
75
|
+
for (const pattern of WARN_PATTERNS) {
|
|
76
|
+
if (pattern.test(html)) {
|
|
77
|
+
warnings.push(`DOM sink detected: ${pattern.source}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { valid: true, warnings };
|
|
82
|
+
}
|
|
@@ -8,12 +8,18 @@ import { api, apiJson, apiFire } from '../api.js';
|
|
|
8
8
|
import { escapeHtml } from '../render.js';
|
|
9
9
|
import { getVirtualScroll } from '../virtual-scroll.js';
|
|
10
10
|
import { clearCache } from './idb-cache.js';
|
|
11
|
+
import { ICONS } from '../icons.js';
|
|
11
12
|
|
|
12
13
|
let activeObjectURLs: string[] = [];
|
|
13
14
|
|
|
14
15
|
interface CommandResult { code?: string; text?: string; type?: string; }
|
|
15
16
|
interface MessageResult { queued?: boolean; pending?: number; continued?: boolean; error?: string; }
|
|
16
17
|
|
|
18
|
+
function getCommandTimeoutMs(text: string): number {
|
|
19
|
+
// Native compaction can take materially longer than the default command round-trip.
|
|
20
|
+
return /^\/compact(?:\s|$)/i.test(String(text || '').trim()) ? 5 * 60 * 1000 : 10_000;
|
|
21
|
+
}
|
|
22
|
+
|
|
17
23
|
export async function sendMessage(): Promise<void> {
|
|
18
24
|
const input = document.getElementById('chatInput') as HTMLTextAreaElement | null;
|
|
19
25
|
const btn = document.getElementById('btnSend');
|
|
@@ -41,12 +47,13 @@ export async function sendMessage(): Promise<void> {
|
|
|
41
47
|
slashCmd.close();
|
|
42
48
|
try {
|
|
43
49
|
let signal: AbortSignal; let timer: ReturnType<typeof setTimeout> | undefined;
|
|
50
|
+
const timeoutMs = getCommandTimeoutMs(text);
|
|
44
51
|
if (typeof AbortSignal?.timeout === 'function') {
|
|
45
|
-
signal = AbortSignal.timeout(
|
|
52
|
+
signal = AbortSignal.timeout(timeoutMs);
|
|
46
53
|
} else {
|
|
47
54
|
const ac = new AbortController();
|
|
48
55
|
signal = ac.signal;
|
|
49
|
-
timer = setTimeout(() => ac.abort(),
|
|
56
|
+
timer = setTimeout(() => ac.abort(), timeoutMs);
|
|
50
57
|
}
|
|
51
58
|
const locale = getPreferredLocale();
|
|
52
59
|
const res = await fetch('/api/command', {
|
|
@@ -72,7 +79,7 @@ export async function sendMessage(): Promise<void> {
|
|
|
72
79
|
const chatEl = document.getElementById('chatMessages');
|
|
73
80
|
if (chatEl) chatEl.innerHTML = '';
|
|
74
81
|
}
|
|
75
|
-
if (result?.text) addSystemMsg(result.text, '', result.type);
|
|
82
|
+
if (result?.text) addSystemMsg(escapeHtml(result.text), '', result.type);
|
|
76
83
|
} catch (err) {
|
|
77
84
|
addSystemMsg(t('chat.cmd.fail', { msg: (err as Error).message }), '', 'error');
|
|
78
85
|
}
|
|
@@ -81,7 +88,7 @@ export async function sendMessage(): Promise<void> {
|
|
|
81
88
|
|
|
82
89
|
if (state.attachedFiles.length) {
|
|
83
90
|
const names = state.attachedFiles.map((f: File) => f.name).join(', ');
|
|
84
|
-
const displayMsg =
|
|
91
|
+
const displayMsg = `📎 [${names}] ${text}`;
|
|
85
92
|
addMessage('user', displayMsg);
|
|
86
93
|
input.value = '';
|
|
87
94
|
resetInputHeight();
|
|
@@ -107,7 +114,7 @@ export async function sendMessage(): Promise<void> {
|
|
|
107
114
|
});
|
|
108
115
|
const data: MessageResult = await res.json().catch(() => ({}));
|
|
109
116
|
if (!res.ok) {
|
|
110
|
-
addSystemMsg(
|
|
117
|
+
addSystemMsg(`${ICONS.error} ${escapeHtml(data.error || t('chat.requestFail', { status: res.status }))}`, '', 'error');
|
|
111
118
|
return;
|
|
112
119
|
}
|
|
113
120
|
if (data.queued) {
|
|
@@ -182,8 +189,8 @@ function renderFilePreview(): void {
|
|
|
182
189
|
}
|
|
183
190
|
return `<div class="file-chip">
|
|
184
191
|
${thumb}
|
|
185
|
-
<span class="file-chip-name"
|
|
186
|
-
<button class="file-chip-remove" data-file-idx="${i}" title="Remove"
|
|
192
|
+
<span class="file-chip-name">${ICONS.paperclip} ${escapeHtml(f.name)} (${size}KB)</span>
|
|
193
|
+
<button class="file-chip-remove" data-file-idx="${i}" title="Remove">${ICONS.close}</button>
|
|
187
194
|
</div>`;
|
|
188
195
|
}).join('');
|
|
189
196
|
}
|
|
@@ -198,10 +205,15 @@ export async function clearChat(): Promise<void> {
|
|
|
198
205
|
clearCache().catch(() => {});
|
|
199
206
|
}
|
|
200
207
|
|
|
201
|
-
// ── Auto-resize textarea ──
|
|
208
|
+
// ── Auto-resize textarea (RAF-batched to avoid blocking input) ──
|
|
209
|
+
let resizeRaf = 0;
|
|
202
210
|
function autoResize(el: HTMLTextAreaElement): void {
|
|
203
|
-
|
|
204
|
-
|
|
211
|
+
if (resizeRaf) return;
|
|
212
|
+
resizeRaf = requestAnimationFrame(() => {
|
|
213
|
+
resizeRaf = 0;
|
|
214
|
+
el.style.height = 'auto';
|
|
215
|
+
el.style.height = el.scrollHeight + 'px';
|
|
216
|
+
});
|
|
205
217
|
}
|
|
206
218
|
|
|
207
219
|
export function initAutoResize(): void {
|
|
@@ -280,7 +292,7 @@ export async function sendVoiceToServer(blob: Blob, ext: string, mime: string):
|
|
|
280
292
|
|
|
281
293
|
// Build user-facing display message
|
|
282
294
|
const displayParts: string[] = ['🎤 [음성 메시지]'];
|
|
283
|
-
if (pendingFiles.length) displayParts.push(
|
|
295
|
+
if (pendingFiles.length) displayParts.push(`📎 [${pendingFiles.map(f => f.name).join(', ')}]`);
|
|
284
296
|
if (pendingText) displayParts.push(pendingText);
|
|
285
297
|
addMessage('user', displayParts.join(' '));
|
|
286
298
|
|
|
@@ -306,7 +318,7 @@ export async function sendVoiceToServer(blob: Blob, ext: string, mime: string):
|
|
|
306
318
|
const sttResult = await sttRes.json().catch(() => null);
|
|
307
319
|
if (!sttResult?.text) throw new Error('Empty STT result');
|
|
308
320
|
|
|
309
|
-
addSystemMsg(
|
|
321
|
+
addSystemMsg(`${ICONS.mic} STT (${escapeHtml(sttResult.engine || '')}, ${sttResult.elapsed?.toFixed(1)}s): "${escapeHtml(sttResult.text.slice(0, 100))}"`, '', 'info');
|
|
310
322
|
|
|
311
323
|
// Step 2: Upload pending files (if any)
|
|
312
324
|
let filePaths: string[] = [];
|
|
@@ -6,6 +6,7 @@ import { escapeHtml } from '../render.js';
|
|
|
6
6
|
import { getAgentPhase } from '../ws.js';
|
|
7
7
|
import { t } from './i18n.js';
|
|
8
8
|
import { api, apiJson, apiFire } from '../api.js';
|
|
9
|
+
import { ICONS } from '../icons.js';
|
|
9
10
|
|
|
10
11
|
interface Employee {
|
|
11
12
|
id: string;
|
|
@@ -80,7 +81,7 @@ export function renderEmployees(): void {
|
|
|
80
81
|
<input style="flex:1;background:none;border:none;color:var(--text);font-size:12px;font-weight:600;font-family:inherit;outline:none"
|
|
81
82
|
value="${escapeHtml(a.name || 'Agent')}"
|
|
82
83
|
data-emp-name="${escapeHtml(a.id)}">
|
|
83
|
-
<button style="background:none;border:none;color:var(--text-dim);cursor:pointer;font-size:12px" data-emp-delete="${escapeHtml(a.id)}" title="${t('emp.delete')}"
|
|
84
|
+
<button style="background:none;border:none;color:var(--text-dim);cursor:pointer;font-size:12px" data-emp-delete="${escapeHtml(a.id)}" title="${t('emp.delete')}">${ICONS.close}</button>
|
|
84
85
|
</div>
|
|
85
86
|
<div style="display:grid;grid-template-columns:1fr 1fr;gap:4px;margin-bottom:4px">
|
|
86
87
|
<div>
|
|
@@ -108,7 +109,7 @@ export function renderEmployees(): void {
|
|
|
108
109
|
placeholder="${t('emp.customRole')}">${isCustom ? escapeHtml(a.role || '') : ''}</textarea>
|
|
109
110
|
</div>
|
|
110
111
|
<div style="margin-top:4px;font-size:10px;display:flex;align-items:center;gap:6px">
|
|
111
|
-
<span style="color:${a.status === 'running' ? '#fbbf24' : 'var(--green)'}"
|
|
112
|
+
<span style="color:${a.status === 'running' ? '#fbbf24' : 'var(--green)'}"><span style="display:inline-block;width:6px;height:6px;border-radius:50%;background:currentColor;vertical-align:middle;margin-right:4px"></span>${escapeHtml(a.status || 'idle')}</span>
|
|
112
113
|
${phaseBadge}
|
|
113
114
|
</div>
|
|
114
115
|
</div>`;
|
|
@@ -4,6 +4,7 @@ import type { HeartbeatJob, HeartbeatSchedule } from '../state.js';
|
|
|
4
4
|
import { t } from './i18n.js';
|
|
5
5
|
import { api, apiJson } from '../api.js';
|
|
6
6
|
import { escapeHtml } from '../render.js';
|
|
7
|
+
import { ICONS } from '../icons.js';
|
|
7
8
|
import {
|
|
8
9
|
validateHeartbeatScheduleInput,
|
|
9
10
|
type HeartbeatScheduleValidationCode,
|
|
@@ -55,7 +56,7 @@ export function renderHeartbeatJobs(): void {
|
|
|
55
56
|
data-hb-name="${i}">
|
|
56
57
|
<button class="hb-toggle ${job.enabled ? 'on' : 'off'}"
|
|
57
58
|
data-hb-toggle="${i}" aria-label="${escapeHtml(String(job.name || 'job') + ' toggle')}"></button>
|
|
58
|
-
<button class="hb-del" data-hb-remove="${i}"
|
|
59
|
+
<button class="hb-del" data-hb-remove="${i}">${ICONS.close}</button>
|
|
59
60
|
</div>
|
|
60
61
|
<div class="hb-job-schedule">
|
|
61
62
|
<select data-hb-kind="${i}">
|
|
@@ -76,7 +77,7 @@ export function renderHeartbeatJobs(): void {
|
|
|
76
77
|
}
|
|
77
78
|
const active = jobs.filter(j => j.enabled).length;
|
|
78
79
|
const btn = document.getElementById('hbSidebarBtn');
|
|
79
|
-
if (btn) btn.
|
|
80
|
+
if (btn) btn.innerHTML = `${ICONS.heartPulse} Heartbeat (${active})`;
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
export function addHeartbeatJob(): void {
|
|
@@ -127,7 +128,7 @@ export async function initHeartbeatBadge(): Promise<void> {
|
|
|
127
128
|
const d = await api<HeartbeatData>('/api/heartbeat');
|
|
128
129
|
const active = (d?.jobs || []).map(normalizeHeartbeatJob).filter(j => j.enabled).length;
|
|
129
130
|
const btn = document.getElementById('hbSidebarBtn');
|
|
130
|
-
if (btn) btn.
|
|
131
|
+
if (btn) btn.innerHTML = `${ICONS.heartPulse} Heartbeat (${active})`;
|
|
131
132
|
} catch { /* ignore */ }
|
|
132
133
|
}
|
|
133
134
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// ── Memory Feature ──
|
|
2
2
|
import { escapeHtml } from '../render.js';
|
|
3
3
|
import { api, apiJson } from '../api.js';
|
|
4
|
+
import { ICONS } from '../icons.js';
|
|
4
5
|
|
|
5
6
|
interface MemoryFile {
|
|
6
7
|
name: string;
|
|
@@ -74,7 +75,7 @@ function syncSidebarBadge(status: AdvancedMemoryStatus | null, basicCount: numbe
|
|
|
74
75
|
: status?.state === 'not_initialized'
|
|
75
76
|
? 'Indexing'
|
|
76
77
|
: (status?.state || `(${basicCount})`);
|
|
77
|
-
sideBtn.
|
|
78
|
+
sideBtn.innerHTML = `${ICONS.brain} Memory · ${escapeHtml(state)}`;
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
function renderStatusBanner(status: AdvancedMemoryStatus | null) {
|
|
@@ -174,7 +175,7 @@ function renderBasicFiles(files: MemoryFile[]) {
|
|
|
174
175
|
<span style="font-size:12px;font-family:monospace">${escapeHtml(f.name)}</span>
|
|
175
176
|
<span style="font-size:10px;color:var(--accent);margin-left:6px">${f.entries} entries</span>
|
|
176
177
|
</div>
|
|
177
|
-
<button data-mem-delete="${escapeHtml(f.name)}" style="background:none;border:none;color:#f55;cursor:pointer;font-size:14px"
|
|
178
|
+
<button data-mem-delete="${escapeHtml(f.name)}" style="background:none;border:none;color:#f55;cursor:pointer;font-size:14px">${ICONS.trash}</button>
|
|
178
179
|
</div>
|
|
179
180
|
`).join('');
|
|
180
181
|
}
|
|
@@ -331,7 +332,7 @@ export async function viewMemFile(name: string): Promise<void> {
|
|
|
331
332
|
container.innerHTML = `
|
|
332
333
|
<div style="margin-bottom:8px;display:flex;justify-content:space-between;align-items:center">
|
|
333
334
|
<span style="font-size:12px;font-weight:600">${escapeHtml(data.name)}</span>
|
|
334
|
-
<button data-mem-back style="background:none;border:none;color:var(--accent);cursor:pointer;font-size:11px"
|
|
335
|
+
<button data-mem-back style="background:none;border:none;color:var(--accent);cursor:pointer;font-size:11px">${ICONS.arrowLeft} back</button>
|
|
335
336
|
</div>
|
|
336
337
|
<pre style="background:var(--bg);padding:8px;border-radius:4px;font-size:11px;white-space:pre-wrap;max-height:50vh;overflow-y:auto;color:var(--text)">${escapeHtml(data.content)}</pre>
|
|
337
338
|
`;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Collapsible panel showing tool/thinking activity during agent responses.
|
|
3
3
|
|
|
4
4
|
import { escapeHtml } from '../render.js';
|
|
5
|
+
import { ICONS } from '../icons.js';
|
|
5
6
|
|
|
6
7
|
export interface ProcessStep {
|
|
7
8
|
id: string;
|
|
@@ -23,13 +24,13 @@ export interface ProcessBlockState {
|
|
|
23
24
|
function buildSummaryText(steps: ProcessStep[]): string {
|
|
24
25
|
const counts: Record<string, number> = {};
|
|
25
26
|
for (const s of steps) {
|
|
26
|
-
const key = s.type === 'thinking' ?
|
|
27
|
-
: s.type === 'search' ?
|
|
28
|
-
:
|
|
27
|
+
const key = s.type === 'thinking' ? `${ICONS.thinking} Thinking`
|
|
28
|
+
: s.type === 'search' ? `${ICONS.search} Search`
|
|
29
|
+
: `${ICONS.tool} Tool`;
|
|
29
30
|
counts[key] = (counts[key] || 0) + 1;
|
|
30
31
|
}
|
|
31
32
|
return Object.entries(counts)
|
|
32
|
-
.map(([k, n]) => n > 1 ? `${k}
|
|
33
|
+
.map(([k, n]) => n > 1 ? `${k}×${n}` : k)
|
|
33
34
|
.join(' + ');
|
|
34
35
|
}
|
|
35
36
|
|
|
@@ -68,7 +69,7 @@ function renderStep(step: ProcessStep): string {
|
|
|
68
69
|
<span class="process-step-label">${label}</span>
|
|
69
70
|
${snippetHtml}
|
|
70
71
|
</span>
|
|
71
|
-
<span class="process-step-chevron"
|
|
72
|
+
<span class="process-step-chevron">${ICONS.chevronRight}</span>
|
|
72
73
|
</button>
|
|
73
74
|
<div class="process-step-details collapsed" id="${detailId}">
|
|
74
75
|
<pre class="process-step-full">${escapeHtml(detail)}</pre>
|
|
@@ -87,9 +88,9 @@ function blockShell(summaryText = '', collapsed = false): string {
|
|
|
87
88
|
return `<div class="process-block${collapsed ? ' collapsed' : ''}">
|
|
88
89
|
<button class="process-summary" aria-expanded="${collapsed ? 'false' : 'true'}">
|
|
89
90
|
<span class="process-dot ${collapsed ? 'done' : 'running'}"></span>
|
|
90
|
-
<span class="process-summary-text">${
|
|
91
|
+
<span class="process-summary-text">${summaryText}</span>
|
|
91
92
|
<span class="process-duration"></span>
|
|
92
|
-
<span class="process-chevron">${collapsed ?
|
|
93
|
+
<span class="process-chevron">${collapsed ? ICONS.chevronRight : ICONS.chevronDown}</span>
|
|
93
94
|
</button>
|
|
94
95
|
<div class="process-details">
|
|
95
96
|
<div class="process-steps-inner"></div>
|
|
@@ -106,7 +107,7 @@ function toggleStepDetails(toggle: HTMLElement): void {
|
|
|
106
107
|
details.classList.toggle('collapsed', !expanding);
|
|
107
108
|
wrapper.classList.toggle('expanded', expanding);
|
|
108
109
|
toggle.setAttribute('aria-expanded', expanding ? 'true' : 'false');
|
|
109
|
-
if (chevron) chevron.
|
|
110
|
+
if (chevron) chevron.innerHTML = expanding ? ICONS.chevronDown : ICONS.chevronRight;
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
export function bindProcessBlockInteractions(root: HTMLElement): void {
|
|
@@ -129,7 +130,7 @@ export function bindProcessBlockInteractions(root: HTMLElement): void {
|
|
|
129
130
|
block.classList.toggle('collapsed', !expanding);
|
|
130
131
|
summary.setAttribute('aria-expanded', expanding ? 'true' : 'false');
|
|
131
132
|
const chevron = summary.querySelector('.process-chevron');
|
|
132
|
-
if (chevron) chevron.
|
|
133
|
+
if (chevron) chevron.innerHTML = expanding ? ICONS.chevronDown : ICONS.chevronRight;
|
|
133
134
|
}
|
|
134
135
|
});
|
|
135
136
|
root.dataset.processBlockBound = '1';
|
|
@@ -153,7 +154,7 @@ export function buildProcessBlockHtml(steps: ProcessStep[], collapsed = true): s
|
|
|
153
154
|
|
|
154
155
|
function updateSummary(pb: ProcessBlockState): void {
|
|
155
156
|
const summaryText = pb.element.querySelector('.process-summary-text');
|
|
156
|
-
if (summaryText) summaryText.
|
|
157
|
+
if (summaryText) summaryText.innerHTML = buildSummaryText(pb.steps);
|
|
157
158
|
|
|
158
159
|
const anyRunning = pb.steps.some(s => s.status === 'running');
|
|
159
160
|
const dot = pb.element.querySelector('.process-dot');
|
|
@@ -223,7 +224,7 @@ export function collapseBlock(pb: ProcessBlockState): void {
|
|
|
223
224
|
const btn = pb.element.querySelector('.process-summary');
|
|
224
225
|
if (btn) btn.setAttribute('aria-expanded', 'false');
|
|
225
226
|
const chevron = pb.element.querySelector('.process-chevron');
|
|
226
|
-
if (chevron) chevron.
|
|
227
|
+
if (chevron) chevron.innerHTML = ICONS.chevronRight;
|
|
227
228
|
|
|
228
229
|
for (const step of pb.steps) {
|
|
229
230
|
if (step.status === 'running') step.status = 'done';
|
|
@@ -3,6 +3,8 @@ import { api } from '../api.js';
|
|
|
3
3
|
import { escapeHtml } from '../render.js';
|
|
4
4
|
import { t } from './i18n.js';
|
|
5
5
|
import { state } from '../state.js';
|
|
6
|
+
import { ICONS } from '../icons.js';
|
|
7
|
+
import { providerIcon } from '../provider-icons.js';
|
|
6
8
|
import type { QuotaEntry } from './settings-types.js';
|
|
7
9
|
|
|
8
10
|
export async function loadCliStatus(force = false): Promise<void> {
|
|
@@ -113,14 +115,15 @@ function renderCliStatus(data: { cliStatus: Record<string, { available: boolean
|
|
|
113
115
|
}).join('');
|
|
114
116
|
} else if (q?.error && info.available) {
|
|
115
117
|
const msg = q.reason === 'rate_limited' ? 'Rate limited — retry in a moment' : 'Usage data unavailable';
|
|
116
|
-
windowsHtml = `<div style="font-size:10px;color:var(--text-dim);margin:2px 0 0 16px;opacity:0.7"
|
|
118
|
+
windowsHtml = `<div style="font-size:10px;color:var(--text-dim);margin:2px 0 0 16px;opacity:0.7">${ICONS.warning} ${msg}</div>`;
|
|
117
119
|
}
|
|
118
120
|
|
|
119
121
|
html += `
|
|
120
122
|
<div class="settings-group" style="margin-bottom:6px;padding:8px 10px">
|
|
121
123
|
<div class="cli-status-row">
|
|
122
124
|
<span class="cli-dot ${dotClass}"></span>
|
|
123
|
-
<span class="cli-
|
|
125
|
+
<span class="cli-provider-icon" aria-hidden="true">${providerIcon(name) || ''}</span>
|
|
126
|
+
<span class="cli-name" style="font-weight:600">${escapeHtml(name)}</span>${name === 'copilot' ? `<button id="copilotKeychainBtn" style="font-size:9px;margin-left:6px;padding:1px 5px;background:var(--border);color:var(--text-dim);border:1px solid var(--text-dim);border-radius:3px;cursor:pointer;vertical-align:middle;line-height:1" title="${t('copilot.keychainHint')}">${ICONS.key}</button>` : ''}
|
|
124
127
|
</div>
|
|
125
128
|
${accountLine}
|
|
126
129
|
${authHint}
|
|
@@ -140,7 +143,7 @@ function renderCliStatus(data: { cliStatus: Record<string, { available: boolean
|
|
|
140
143
|
if (!hasReadyCli && allEntries.length > 0 && el) {
|
|
141
144
|
el.insertAdjacentHTML('afterbegin',
|
|
142
145
|
`<div style="padding:8px 10px;margin-bottom:8px;background:#fbbf2422;border:1px solid #fbbf24;border-radius:6px;font-size:11px;color:#fbbf24">
|
|
143
|
-
|
|
146
|
+
${ICONS.warning} ${t('cli.noReadyCli')}
|
|
144
147
|
</div>`
|
|
145
148
|
);
|
|
146
149
|
}
|
|
@@ -150,15 +153,15 @@ function renderCliStatus(data: { cliStatus: Record<string, { available: boolean
|
|
|
150
153
|
kcBtn.addEventListener('click', async () => {
|
|
151
154
|
const btn = kcBtn as HTMLButtonElement;
|
|
152
155
|
btn.disabled = true;
|
|
153
|
-
btn.
|
|
156
|
+
btn.innerHTML = ICONS.hourglass;
|
|
154
157
|
try {
|
|
155
158
|
const res = await api<{ ok: boolean }>('/api/copilot/refresh', { method: 'POST' });
|
|
156
|
-
btn.
|
|
159
|
+
btn.innerHTML = res?.ok ? ICONS.check : ICONS.error;
|
|
157
160
|
if (res?.ok) await loadCliStatus(true);
|
|
158
161
|
} catch {
|
|
159
|
-
btn.
|
|
162
|
+
btn.innerHTML = ICONS.error;
|
|
160
163
|
}
|
|
161
|
-
setTimeout(() => { btn.
|
|
164
|
+
setTimeout(() => { btn.innerHTML = ICONS.key; btn.disabled = false; }, 2000);
|
|
162
165
|
});
|
|
163
166
|
}
|
|
164
167
|
}
|
|
@@ -10,6 +10,7 @@ import { loadTelegramSettings } from './settings-telegram.js';
|
|
|
10
10
|
import { loadDiscordSettings } from './settings-discord.js';
|
|
11
11
|
import { loadActiveChannel, loadFallbackOrder } from './settings-channel.js';
|
|
12
12
|
import { loadMcpServers } from './settings-mcp.js';
|
|
13
|
+
import { providerIcon } from '../provider-icons.js';
|
|
13
14
|
|
|
14
15
|
function toCap(cli: string): string {
|
|
15
16
|
return cli.charAt(0).toUpperCase() + cli.slice(1);
|
|
@@ -150,7 +151,10 @@ export async function loadSettings(): Promise<void> {
|
|
|
150
151
|
const cwdEl = document.getElementById('inpCwd');
|
|
151
152
|
if (cwdEl) cwdEl.textContent = s.workingDir;
|
|
152
153
|
const headerEl = document.getElementById('headerCli');
|
|
153
|
-
if (headerEl)
|
|
154
|
+
if (headerEl) {
|
|
155
|
+
const icon = providerIcon(s.cli);
|
|
156
|
+
headerEl.innerHTML = icon ? `${icon} ${escapeHtml(s.cli)}` : escapeHtml(s.cli);
|
|
157
|
+
}
|
|
154
158
|
setPerm(s.permissions, false);
|
|
155
159
|
|
|
156
160
|
if (s.perCli) {
|
|
@@ -214,7 +218,10 @@ export async function updateSettings(): Promise<void> {
|
|
|
214
218
|
cli: (document.getElementById('selCli') as HTMLSelectElement)?.value || 'claude',
|
|
215
219
|
};
|
|
216
220
|
const hdr = document.getElementById('headerCli');
|
|
217
|
-
if (hdr)
|
|
221
|
+
if (hdr) {
|
|
222
|
+
const ico = providerIcon(s.cli);
|
|
223
|
+
hdr.innerHTML = ico ? `${ico} ${escapeHtml(s.cli)}` : escapeHtml(s.cli);
|
|
224
|
+
}
|
|
218
225
|
await apiJson('/api/settings', 'PUT', s);
|
|
219
226
|
}
|
|
220
227
|
|
|
@@ -295,7 +302,10 @@ export function onCliChange(save = true): void {
|
|
|
295
302
|
const modelSel = document.getElementById('selModel') as HTMLSelectElement | null;
|
|
296
303
|
setSelectOptions(modelSel, models, { includeCustom: true, includeDefault: true });
|
|
297
304
|
const hdrCli = document.getElementById('headerCli');
|
|
298
|
-
if (hdrCli)
|
|
305
|
+
if (hdrCli) {
|
|
306
|
+
const ico = providerIcon(cli);
|
|
307
|
+
hdrCli.innerHTML = ico ? `${ico} ${escapeHtml(cli)}` : escapeHtml(cli);
|
|
308
|
+
}
|
|
299
309
|
syncActiveEffortOptions(cli);
|
|
300
310
|
|
|
301
311
|
const oldInput = document.getElementById('selModelCustom');
|
|
@@ -30,6 +30,12 @@ export async function setDiscordAllowBots(allow: boolean): Promise<void> {
|
|
|
30
30
|
await apiJson('/api/settings', 'PUT', { discord: { allowBots: allow } });
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
export async function setDiscordMentionOnly(enabled: boolean): Promise<void> {
|
|
34
|
+
document.getElementById('dcMentionOn')?.classList.toggle('active', enabled);
|
|
35
|
+
document.getElementById('dcMentionOff')?.classList.toggle('active', !enabled);
|
|
36
|
+
await apiJson('/api/settings', 'PUT', { discord: { mentionOnly: enabled } });
|
|
37
|
+
}
|
|
38
|
+
|
|
33
39
|
export function loadDiscordSettings(s: SettingsData): void {
|
|
34
40
|
if (!s.discord) return;
|
|
35
41
|
const dc = s.discord;
|
|
@@ -49,4 +55,7 @@ export function loadDiscordSettings(s: SettingsData): void {
|
|
|
49
55
|
const allowBots = !!dc.allowBots;
|
|
50
56
|
document.getElementById('dcAllowBotsOn')?.classList.toggle('active', allowBots);
|
|
51
57
|
document.getElementById('dcAllowBotsOff')?.classList.toggle('active', !allowBots);
|
|
58
|
+
const mentionOnly = !!dc.mentionOnly;
|
|
59
|
+
document.getElementById('dcMentionOn')?.classList.toggle('active', mentionOnly);
|
|
60
|
+
document.getElementById('dcMentionOff')?.classList.toggle('active', !mentionOnly);
|
|
52
61
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { api, apiJson } from '../api.js';
|
|
3
3
|
import { escapeHtml } from '../render.js';
|
|
4
4
|
import { t } from './i18n.js';
|
|
5
|
+
import { ICONS } from '../icons.js';
|
|
5
6
|
|
|
6
7
|
interface McpData { servers: Record<string, { command: string; args?: string[] }>; }
|
|
7
8
|
interface McpSyncResult { results: Record<string, boolean>; }
|
|
@@ -29,12 +30,12 @@ export async function syncMcpServers(): Promise<void> {
|
|
|
29
30
|
resultEl.textContent = t('mcp.syncing');
|
|
30
31
|
try {
|
|
31
32
|
const d = await apiJson('/api/mcp/sync', 'POST', {}) as McpSyncResult | null;
|
|
32
|
-
if (!d) { resultEl.
|
|
33
|
+
if (!d) { resultEl.innerHTML = `${ICONS.error} sync failed`; return; }
|
|
33
34
|
const r = d.results || {};
|
|
34
35
|
resultEl.innerHTML = Object.entries(r).map(([k, v]) =>
|
|
35
|
-
`${v ?
|
|
36
|
+
`${v ? ICONS.check : ICONS.skip} ${escapeHtml(k)}`
|
|
36
37
|
).join(' ');
|
|
37
|
-
} catch (e) { resultEl.
|
|
38
|
+
} catch (e) { resultEl.innerHTML = `${ICONS.error} ${escapeHtml((e as Error).message)}`; }
|
|
38
39
|
}
|
|
39
40
|
|
|
40
41
|
export async function installMcpGlobal(): Promise<void> {
|
|
@@ -44,11 +45,11 @@ export async function installMcpGlobal(): Promise<void> {
|
|
|
44
45
|
resultEl.textContent = t('mcp.installing');
|
|
45
46
|
try {
|
|
46
47
|
const d = await apiJson('/api/mcp/install', 'POST', {}) as McpInstallResult | null;
|
|
47
|
-
if (!d) { resultEl.
|
|
48
|
+
if (!d) { resultEl.innerHTML = `${ICONS.error} install failed`; return; }
|
|
48
49
|
resultEl.innerHTML = Object.entries(d.results || {}).map(([k, v]) => {
|
|
49
|
-
const
|
|
50
|
-
return `${
|
|
50
|
+
const ic = v.status === 'installed' ? ICONS.check : v.status === 'skip' ? ICONS.skip : ICONS.error;
|
|
51
|
+
return `${ic} <b>${escapeHtml(k)}</b>: ${escapeHtml(v.status)}${v.bin ? ` ${ICONS.arrowRight} ` + escapeHtml(v.bin) : ''}`;
|
|
51
52
|
}).join('<br>');
|
|
52
53
|
loadMcpServers();
|
|
53
|
-
} catch (e) { resultEl.
|
|
54
|
+
} catch (e) { resultEl.innerHTML = `${ICONS.error} ${escapeHtml((e as Error).message)}`; }
|
|
54
55
|
}
|
|
@@ -13,7 +13,7 @@ export function initSttSettings(sttConfig: Record<string, any>): void {
|
|
|
13
13
|
const vertexJson = document.getElementById('sttVertexJson') as HTMLTextAreaElement | null;
|
|
14
14
|
|
|
15
15
|
if (engine) engine.value = sttConfig.engine || 'auto';
|
|
16
|
-
if (geminiKey) geminiKey.placeholder = sttConfig.geminiKeySet ?
|
|
16
|
+
if (geminiKey) geminiKey.placeholder = sttConfig.geminiKeySet ? `✓ 입력됨 ····${sttConfig.geminiKeyLast4 || ''}` : 'AIza...';
|
|
17
17
|
if (geminiModel) {
|
|
18
18
|
const saved = sttConfig.geminiModel || 'gemini-2.5-flash-lite';
|
|
19
19
|
const hasOption = Array.from(geminiModel.options).some(o => o.value === saved);
|
|
@@ -22,7 +22,7 @@ export function initSttSettings(sttConfig: Record<string, any>): void {
|
|
|
22
22
|
}
|
|
23
23
|
if (whisperModel) whisperModel.value = sttConfig.whisperModel || 'mlx-community/whisper-large-v3-turbo';
|
|
24
24
|
if (openaiBaseUrl) openaiBaseUrl.value = sttConfig.openaiBaseUrl || '';
|
|
25
|
-
if (openaiKey) openaiKey.placeholder = sttConfig.openaiKeySet ?
|
|
25
|
+
if (openaiKey) openaiKey.placeholder = sttConfig.openaiKeySet ? `✓ 입력됨 ····${sttConfig.openaiKeyLast4 || ''}` : 'sk-...';
|
|
26
26
|
if (openaiModel) openaiModel.value = sttConfig.openaiModel || '';
|
|
27
27
|
if (vertexJson) vertexJson.value = sttConfig.vertexConfig || '';
|
|
28
28
|
|
|
@@ -55,8 +55,8 @@ export function initSttSettings(sttConfig: Record<string, any>): void {
|
|
|
55
55
|
console.log('[stt] saving:', { engine: patch.stt.engine, hasGeminiKey: !!patch.stt.geminiApiKey, hasOpenaiKey: !!patch.stt.openaiApiKey });
|
|
56
56
|
try {
|
|
57
57
|
await apiJson('/api/settings', 'PUT', patch);
|
|
58
|
-
if (geminiKey?.value) { const l4 = geminiKey.value.slice(-4); geminiKey.value = ''; geminiKey.placeholder =
|
|
59
|
-
if (openaiKey?.value) { const l4 = openaiKey.value.slice(-4); openaiKey.value = ''; openaiKey.placeholder =
|
|
58
|
+
if (geminiKey?.value) { const l4 = geminiKey.value.slice(-4); geminiKey.value = ''; geminiKey.placeholder = `✓ 입력됨 ····${l4}`; }
|
|
59
|
+
if (openaiKey?.value) { const l4 = openaiKey.value.slice(-4); openaiKey.value = ''; openaiKey.placeholder = `✓ 입력됨 ····${l4}`; }
|
|
60
60
|
} catch (e) {
|
|
61
61
|
console.error('[stt] save failed:', e);
|
|
62
62
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
// ── Prompt & Template Modals ──
|
|
2
2
|
import { api, apiJson } from '../api.js';
|
|
3
|
+
import { ICONS } from '../icons.js';
|
|
4
|
+
import { escapeHtml } from '../render.js';
|
|
3
5
|
|
|
4
6
|
// ── Prompt Modal ──
|
|
5
7
|
|
|
@@ -55,7 +57,7 @@ function renderTree(tree: TreeNode[]): void {
|
|
|
55
57
|
if (!tmpl) continue;
|
|
56
58
|
const node = document.createElement('div');
|
|
57
59
|
node.style.cssText = 'background:var(--bg);border:1px solid var(--border);border-radius:6px;padding:6px 10px;margin:2px 0 2px 24px;font-size:12px;cursor:pointer;transition:border-color .15s';
|
|
58
|
-
node.
|
|
60
|
+
node.innerHTML = `${ICONS.file} ${escapeHtml(tmpl.filename)}`;
|
|
59
61
|
node.addEventListener('mouseenter', () => { node.style.borderColor = 'var(--accent2)'; });
|
|
60
62
|
node.addEventListener('mouseleave', () => { node.style.borderColor = 'var(--border)'; });
|
|
61
63
|
node.addEventListener('click', () => { openTemplateEditor(tmpl); });
|
|
@@ -71,22 +73,22 @@ function openTemplateEditor(tmpl: TemplateInfo): void {
|
|
|
71
73
|
editor.readOnly = true;
|
|
72
74
|
_devMode = false;
|
|
73
75
|
const label = document.getElementById('templateEditorLabel');
|
|
74
|
-
if (label) label.
|
|
76
|
+
if (label) label.innerHTML = `${ICONS.file} ${escapeHtml(tmpl.filename)}`;
|
|
75
77
|
const vars = tmpl.content.match(/\{\{[A-Z_]+\}\}/g);
|
|
76
78
|
const varsEl = document.getElementById('templateVars');
|
|
77
79
|
if (varsEl) varsEl.textContent = vars ? `vars: ${[...new Set(vars)].join(', ')}` : 'no variables';
|
|
78
80
|
const saveBtn = document.getElementById('templateSaveBtn');
|
|
79
81
|
if (saveBtn) saveBtn.style.display = 'none';
|
|
80
82
|
const toggle = document.getElementById('templateDevToggle');
|
|
81
|
-
if (toggle) { toggle.style.color = 'var(--text-dim)'; toggle.style.borderColor = 'var(--border)'; toggle.
|
|
83
|
+
if (toggle) { toggle.style.color = 'var(--text-dim)'; toggle.style.borderColor = 'var(--border)'; toggle.innerHTML = `${ICONS.tool} 개발자 모드`; }
|
|
82
84
|
const title = document.getElementById('templateModalTitle');
|
|
83
|
-
if (title) title.
|
|
85
|
+
if (title) title.innerHTML = `${ICONS.file} ${escapeHtml(tmpl.filename)}`;
|
|
84
86
|
showTemplateView('editor');
|
|
85
87
|
}
|
|
86
88
|
|
|
87
89
|
export function toggleDevMode(): void {
|
|
88
90
|
if (!_devMode) {
|
|
89
|
-
if (!confirm('
|
|
91
|
+
if (!confirm('⚠ 프롬프트를 직접 수정하면 예상치 못한 동작이 발생할 수 있습니다.\n계속하시겠습니까?')) return;
|
|
90
92
|
}
|
|
91
93
|
_devMode = !_devMode;
|
|
92
94
|
const editor = document.getElementById('templateEditor') as HTMLTextAreaElement;
|
|
@@ -97,7 +99,7 @@ export function toggleDevMode(): void {
|
|
|
97
99
|
if (toggle) {
|
|
98
100
|
toggle.style.color = _devMode ? 'var(--stop-btn)' : 'var(--text-dim)';
|
|
99
101
|
toggle.style.borderColor = _devMode ? 'var(--stop-btn)' : 'var(--border)';
|
|
100
|
-
toggle.
|
|
102
|
+
toggle.innerHTML = _devMode ? `${ICONS.lockOpen} 개발자 모드 ON` : `${ICONS.tool} 개발자 모드`;
|
|
101
103
|
}
|
|
102
104
|
}
|
|
103
105
|
|
|
@@ -107,7 +109,7 @@ export async function saveTemplateFromModal(): Promise<void> {
|
|
|
107
109
|
if (!id) return;
|
|
108
110
|
await apiJson(`/api/prompt-templates/${id}`, 'PUT', { content: editor.value });
|
|
109
111
|
const label = document.getElementById('templateEditorLabel');
|
|
110
|
-
if (label) { label.
|
|
112
|
+
if (label) { label.innerHTML = `${ICONS.check} 저장 + 핫리로드 완료!`; setTimeout(() => { label.innerHTML = `${ICONS.file} ${escapeHtml(id)}.md`; }, 2000); }
|
|
111
113
|
const t = _templates.find(x => x.id === id);
|
|
112
114
|
if (t) t.content = editor.value;
|
|
113
115
|
}
|
|
@@ -118,7 +120,7 @@ function showTemplateView(view: 'tree' | 'editor'): void {
|
|
|
118
120
|
if (treeView) treeView.style.display = view === 'tree' ? '' : 'none';
|
|
119
121
|
if (editorView) editorView.style.display = view === 'editor' ? 'flex' : 'none';
|
|
120
122
|
const title = document.getElementById('templateModalTitle');
|
|
121
|
-
if (title && view === 'tree') title.
|
|
123
|
+
if (title && view === 'tree') title.innerHTML = `${ICONS.plan} 프롬프트 구조`;
|
|
122
124
|
}
|
|
123
125
|
|
|
124
126
|
export function templateGoBack(): void { showTemplateView('tree'); }
|