cli-jaw 0.1.10 → 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/agent/spawn.js +22 -8
- package/dist/src/agent/spawn.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/dist/src/telegram/bot.js +1 -1
- package/dist/src/telegram/bot.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
package/public/index.html
CHANGED
|
@@ -25,9 +25,7 @@
|
|
|
25
25
|
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.16/dist/katex.min.js"></script>
|
|
26
26
|
<script defer src="https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.min.js"></script>
|
|
27
27
|
<script defer src="https://cdn.jsdelivr.net/npm/dompurify@3/dist/purify.min.js"></script>
|
|
28
|
-
|
|
29
|
-
<link rel="modulepreload" href="/js/ws.js">
|
|
30
|
-
<link rel="modulepreload" href="/js/ui.js">
|
|
28
|
+
<!-- modulepreload removed: esbuild bundles all modules into dist/bundle.js -->
|
|
31
29
|
</head>
|
|
32
30
|
|
|
33
31
|
<body>
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
// ── API Fetch Wrapper ──
|
|
2
2
|
// All API calls centralized for error handling + ok/data unwrapping
|
|
3
3
|
|
|
4
|
+
interface ApiResponse<T = unknown> {
|
|
5
|
+
ok?: boolean;
|
|
6
|
+
data?: T;
|
|
7
|
+
error?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
4
10
|
/**
|
|
5
|
-
* @param
|
|
6
|
-
* @param
|
|
7
|
-
* @returns
|
|
11
|
+
* @param path - API path (e.g. '/api/settings')
|
|
12
|
+
* @param opts - fetch options
|
|
13
|
+
* @returns data on success, null on failure
|
|
8
14
|
*/
|
|
9
|
-
export async function api(path, opts = {}) {
|
|
15
|
+
export async function api<T = unknown>(path: string, opts: RequestInit = {}): Promise<T | null> {
|
|
10
16
|
try {
|
|
11
17
|
const res = await fetch(path, opts);
|
|
12
18
|
if (!res.ok) {
|
|
@@ -15,18 +21,18 @@ export async function api(path, opts = {}) {
|
|
|
15
21
|
}
|
|
16
22
|
const contentType = res.headers.get('content-type') || '';
|
|
17
23
|
if (!contentType.includes('json')) return null;
|
|
18
|
-
const json = await res.json()
|
|
24
|
+
const json = (await res.json()) as ApiResponse<T>;
|
|
19
25
|
// Phase 9.2 dual-response compat: { ok, data } or bare response
|
|
20
26
|
if (json && typeof json === 'object' && 'ok' in json && 'data' in json) {
|
|
21
27
|
if (!json.ok) {
|
|
22
28
|
console.warn(`[api] ${path} → ok:false`, json.error || '');
|
|
23
29
|
return null;
|
|
24
30
|
}
|
|
25
|
-
return json.data;
|
|
31
|
+
return json.data as T;
|
|
26
32
|
}
|
|
27
|
-
return json;
|
|
33
|
+
return json as unknown as T;
|
|
28
34
|
} catch (e) {
|
|
29
|
-
console.warn(`[api] ${path} failed:`, e.message);
|
|
35
|
+
console.warn(`[api] ${path} failed:`, (e as Error).message);
|
|
30
36
|
return null;
|
|
31
37
|
}
|
|
32
38
|
}
|
|
@@ -34,8 +40,8 @@ export async function api(path, opts = {}) {
|
|
|
34
40
|
/**
|
|
35
41
|
* POST/PUT/DELETE JSON request
|
|
36
42
|
*/
|
|
37
|
-
export async function apiJson(path, method, body) {
|
|
38
|
-
return api(path, {
|
|
43
|
+
export async function apiJson<T = unknown>(path: string, method: string, body: unknown): Promise<T | null> {
|
|
44
|
+
return api<T>(path, {
|
|
39
45
|
method,
|
|
40
46
|
headers: { 'Content-Type': 'application/json' },
|
|
41
47
|
body: JSON.stringify(body),
|
|
@@ -45,8 +51,8 @@ export async function apiJson(path, method, body) {
|
|
|
45
51
|
/**
|
|
46
52
|
* fire-and-forget: ignores result
|
|
47
53
|
*/
|
|
48
|
-
export function apiFire(path, method = 'POST', body) {
|
|
49
|
-
const opts = { method };
|
|
54
|
+
export function apiFire(path: string, method: string = 'POST', body?: unknown): void {
|
|
55
|
+
const opts: RequestInit = { method };
|
|
50
56
|
if (body) {
|
|
51
57
|
opts.headers = { 'Content-Type': 'application/json' };
|
|
52
58
|
opts.body = JSON.stringify(body);
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
// ── Shared constants (frontend) ──
|
|
2
2
|
import { api } from './api.js';
|
|
3
3
|
|
|
4
|
-
|
|
4
|
+
export interface CliEntry {
|
|
5
|
+
label: string;
|
|
6
|
+
efforts: string[];
|
|
7
|
+
models: string[];
|
|
8
|
+
effortNote?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type CliRegistry = Record<string, CliEntry>;
|
|
12
|
+
|
|
13
|
+
const FALLBACK_CLI_REGISTRY: CliRegistry = {
|
|
5
14
|
claude: {
|
|
6
15
|
label: 'Claude',
|
|
7
16
|
efforts: ['low', 'medium', 'high'],
|
|
@@ -53,36 +62,39 @@ const FALLBACK_CLI_REGISTRY = {
|
|
|
53
62
|
},
|
|
54
63
|
};
|
|
55
64
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
65
|
+
type ModelMap = Record<string, string[]>;
|
|
66
|
+
|
|
67
|
+
function toModelMap(registry: CliRegistry): ModelMap {
|
|
68
|
+
const out: ModelMap = {};
|
|
69
|
+
for (const [key, value] of Object.entries(registry)) {
|
|
59
70
|
out[key] = Array.isArray(value?.models) ? [...value.models] : [];
|
|
60
71
|
}
|
|
61
72
|
return out;
|
|
62
73
|
}
|
|
63
74
|
|
|
64
|
-
function normalizeRegistry(input) {
|
|
65
|
-
const out = {};
|
|
75
|
+
function normalizeRegistry(input: Record<string, unknown>): CliRegistry {
|
|
76
|
+
const out: CliRegistry = {};
|
|
66
77
|
for (const [key, value] of Object.entries(input || {})) {
|
|
67
78
|
if (!value || typeof value !== 'object') continue;
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
79
|
+
const v = value as Record<string, unknown>;
|
|
80
|
+
const normalized: CliEntry = {
|
|
81
|
+
label: (v.label as string) || key,
|
|
82
|
+
efforts: Array.isArray(v.efforts) ? [...v.efforts] as string[] : [],
|
|
83
|
+
models: Array.isArray(v.models) ? [...v.models] as string[] : [],
|
|
72
84
|
};
|
|
73
|
-
if (typeof
|
|
74
|
-
normalized.effortNote =
|
|
85
|
+
if (typeof v.effortNote === 'string' && v.effortNote.trim()) {
|
|
86
|
+
normalized.effortNote = v.effortNote;
|
|
75
87
|
}
|
|
76
88
|
out[key] = normalized;
|
|
77
89
|
}
|
|
78
90
|
return out;
|
|
79
91
|
}
|
|
80
92
|
|
|
81
|
-
export let CLI_REGISTRY = normalizeRegistry(FALLBACK_CLI_REGISTRY);
|
|
82
|
-
export let CLI_KEYS = Object.keys(CLI_REGISTRY);
|
|
83
|
-
export let MODEL_MAP = toModelMap(CLI_REGISTRY);
|
|
93
|
+
export let CLI_REGISTRY: CliRegistry = normalizeRegistry(FALLBACK_CLI_REGISTRY as unknown as Record<string, unknown>);
|
|
94
|
+
export let CLI_KEYS: string[] = Object.keys(CLI_REGISTRY);
|
|
95
|
+
export let MODEL_MAP: ModelMap = toModelMap(CLI_REGISTRY);
|
|
84
96
|
|
|
85
|
-
function applyRegistry(registry) {
|
|
97
|
+
function applyRegistry(registry: Record<string, unknown>): boolean {
|
|
86
98
|
const normalized = normalizeRegistry(registry);
|
|
87
99
|
if (!Object.keys(normalized).length) return false;
|
|
88
100
|
CLI_REGISTRY = normalized;
|
|
@@ -91,29 +103,37 @@ function applyRegistry(registry) {
|
|
|
91
103
|
return true;
|
|
92
104
|
}
|
|
93
105
|
|
|
94
|
-
export async function loadCliRegistry() {
|
|
106
|
+
export async function loadCliRegistry(): Promise<CliRegistry> {
|
|
95
107
|
try {
|
|
96
|
-
const data = await api('/api/cli-registry');
|
|
108
|
+
const data = await api<Record<string, unknown>>('/api/cli-registry');
|
|
97
109
|
if (!data || !applyRegistry(data)) throw new Error('invalid registry');
|
|
98
110
|
} catch (e) {
|
|
99
|
-
console.warn('[cli-registry] fallback:', e.message);
|
|
100
|
-
applyRegistry(FALLBACK_CLI_REGISTRY);
|
|
111
|
+
console.warn('[cli-registry] fallback:', (e as Error).message);
|
|
112
|
+
applyRegistry(FALLBACK_CLI_REGISTRY as unknown as Record<string, unknown>);
|
|
101
113
|
}
|
|
102
114
|
return CLI_REGISTRY;
|
|
103
115
|
}
|
|
104
116
|
|
|
105
|
-
export function getCliKeys() {
|
|
117
|
+
export function getCliKeys(): string[] {
|
|
106
118
|
return CLI_KEYS;
|
|
107
119
|
}
|
|
108
120
|
|
|
109
|
-
export function getCliMeta(cli) {
|
|
121
|
+
export function getCliMeta(cli: string): CliEntry | null {
|
|
110
122
|
return CLI_REGISTRY[cli] || null;
|
|
111
123
|
}
|
|
112
124
|
|
|
113
|
-
export
|
|
125
|
+
export interface RolePreset {
|
|
126
|
+
value: string;
|
|
127
|
+
labelKey: string;
|
|
128
|
+
label: string;
|
|
129
|
+
prompt: string;
|
|
130
|
+
skill: string | null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export const ROLE_PRESETS: readonly RolePreset[] = [
|
|
114
134
|
{ value: 'frontend', labelKey: 'role.label.frontend', label: 'Frontend', prompt: 'UI/UX, CSS, components', skill: 'dev-frontend' },
|
|
115
135
|
{ value: 'backend', labelKey: 'role.label.backend', label: 'Backend', prompt: 'API, DB, server logic', skill: 'dev-backend' },
|
|
116
136
|
{ value: 'data', labelKey: 'role.label.data', label: 'Data', prompt: 'Data pipeline, analysis, ML', skill: 'dev-data' },
|
|
117
137
|
{ value: 'docs', labelKey: 'role.label.docs', label: 'Docs', prompt: 'Documentation, README, API docs', skill: 'documentation' },
|
|
118
138
|
{ value: 'custom', labelKey: 'role.label.custom', label: 'Custom...', prompt: '', skill: null },
|
|
119
|
-
];
|
|
139
|
+
] as const;
|
|
@@ -5,39 +5,40 @@
|
|
|
5
5
|
const STORAGE_KEY = 'agentName';
|
|
6
6
|
const DEFAULT_NAME = 'CLI-JAW';
|
|
7
7
|
|
|
8
|
-
let currentName = DEFAULT_NAME;
|
|
8
|
+
let currentName: string = DEFAULT_NAME;
|
|
9
9
|
|
|
10
|
-
export function getAppName() {
|
|
10
|
+
export function getAppName(): string {
|
|
11
11
|
return currentName;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
export function setAppName(name) {
|
|
14
|
+
export function setAppName(name: string): void {
|
|
15
15
|
currentName = (name || '').trim() || DEFAULT_NAME;
|
|
16
16
|
localStorage.setItem(STORAGE_KEY, currentName);
|
|
17
17
|
// Update input field
|
|
18
|
-
const input = document.getElementById('appNameInput');
|
|
18
|
+
const input = document.getElementById('appNameInput') as HTMLInputElement | null;
|
|
19
19
|
if (input) input.value = currentName;
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
export function initAppName() {
|
|
22
|
+
export function initAppName(): void {
|
|
23
23
|
currentName = localStorage.getItem(STORAGE_KEY) || DEFAULT_NAME;
|
|
24
24
|
|
|
25
25
|
// Sync input
|
|
26
|
-
const input = document.getElementById('appNameInput');
|
|
26
|
+
const input = document.getElementById('appNameInput') as HTMLInputElement | null;
|
|
27
27
|
if (input) input.value = currentName;
|
|
28
28
|
|
|
29
29
|
// Save button
|
|
30
30
|
document.getElementById('appNameSave')?.addEventListener('click', () => {
|
|
31
|
-
const inp = document.getElementById('appNameInput');
|
|
31
|
+
const inp = document.getElementById('appNameInput') as HTMLInputElement | null;
|
|
32
32
|
if (inp) setAppName(inp.value);
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
// Enter key
|
|
36
|
-
document.getElementById('appNameInput')?.addEventListener('keydown', (e) => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
document.getElementById('appNameInput')?.addEventListener('keydown', (e: Event) => {
|
|
37
|
+
const ke = e as KeyboardEvent;
|
|
38
|
+
if (ke.key === 'Enter') {
|
|
39
|
+
ke.preventDefault();
|
|
40
|
+
setAppName((ke.target as HTMLInputElement).value);
|
|
41
|
+
(ke.target as HTMLInputElement).blur();
|
|
41
42
|
}
|
|
42
43
|
});
|
|
43
44
|
}
|
|
@@ -6,9 +6,13 @@ import { t } from './i18n.js';
|
|
|
6
6
|
import * as slashCmd from './slash-commands.js';
|
|
7
7
|
import { api, apiJson, apiFire } from '../api.js';
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
interface CommandResult { code?: string; text?: string; type?: string; }
|
|
10
|
+
interface MessageResult { queued?: boolean; pending?: number; continued?: boolean; error?: string; }
|
|
11
|
+
|
|
12
|
+
export async function sendMessage(): Promise<void> {
|
|
13
|
+
const input = document.getElementById('chatInput') as HTMLTextAreaElement | null;
|
|
11
14
|
const btn = document.getElementById('btnSend');
|
|
15
|
+
if (!input || !btn) return;
|
|
12
16
|
|
|
13
17
|
// Stop mode: clicking ■ stops the agent
|
|
14
18
|
if (btn.classList.contains('stop-mode') && !input.value.trim() && !state.attachedFiles.length) {
|
|
@@ -29,7 +33,7 @@ export async function sendMessage() {
|
|
|
29
33
|
resetInputHeight();
|
|
30
34
|
slashCmd.close();
|
|
31
35
|
try {
|
|
32
|
-
let signal
|
|
36
|
+
let signal: AbortSignal; let timer: ReturnType<typeof setTimeout> | undefined;
|
|
33
37
|
if (typeof AbortSignal?.timeout === 'function') {
|
|
34
38
|
signal = AbortSignal.timeout(10000);
|
|
35
39
|
} else {
|
|
@@ -48,7 +52,7 @@ export async function sendMessage() {
|
|
|
48
52
|
signal,
|
|
49
53
|
});
|
|
50
54
|
if (timer) clearTimeout(timer);
|
|
51
|
-
const result = await res.json().catch(() => ({}));
|
|
55
|
+
const result: CommandResult = await res.json().catch(() => ({}));
|
|
52
56
|
// not_command → fall through to normal chat
|
|
53
57
|
if (result?.code === 'not_command') {
|
|
54
58
|
addMessage('user', text);
|
|
@@ -57,30 +61,31 @@ export async function sendMessage() {
|
|
|
57
61
|
}
|
|
58
62
|
if (!res.ok && !result?.text) throw new Error(`HTTP ${res.status}`);
|
|
59
63
|
if (result?.code === 'clear_screen') {
|
|
60
|
-
document.getElementById('chatMessages')
|
|
64
|
+
const chatEl = document.getElementById('chatMessages');
|
|
65
|
+
if (chatEl) chatEl.innerHTML = '';
|
|
61
66
|
}
|
|
62
67
|
if (result?.text) addSystemMsg(result.text, '', result.type);
|
|
63
68
|
} catch (err) {
|
|
64
|
-
addSystemMsg(t('chat.cmd.fail', { msg: err.message }), '', 'error');
|
|
69
|
+
addSystemMsg(t('chat.cmd.fail', { msg: (err as Error).message }), '', 'error');
|
|
65
70
|
}
|
|
66
71
|
return;
|
|
67
72
|
}
|
|
68
73
|
|
|
69
74
|
if (state.attachedFiles.length) {
|
|
70
|
-
const names = state.attachedFiles.map(f => f.name).join(', ');
|
|
75
|
+
const names = state.attachedFiles.map((f: File) => f.name).join(', ');
|
|
71
76
|
const displayMsg = `[📎 ${names}] ${text}`;
|
|
72
77
|
addMessage('user', displayMsg);
|
|
73
78
|
input.value = '';
|
|
74
79
|
resetInputHeight();
|
|
75
80
|
try {
|
|
76
81
|
// Upload all files in parallel
|
|
77
|
-
const paths = await Promise.all(state.attachedFiles.map(f => uploadFile(f)));
|
|
82
|
+
const paths = await Promise.all(state.attachedFiles.map((f: File) => uploadFile(f)));
|
|
78
83
|
let prompt = paths.map(p => t('chat.file.sent', { path: p })).join('\n');
|
|
79
84
|
if (text) prompt += t('chat.file.sentWithMsg', { text });
|
|
80
85
|
clearAttachedFiles();
|
|
81
86
|
await apiJson('/api/message', 'POST', { prompt });
|
|
82
87
|
} catch (err) {
|
|
83
|
-
addSystemMsg(t('chat.file.uploadFail', { msg: err.message }));
|
|
88
|
+
addSystemMsg(t('chat.file.uploadFail', { msg: (err as Error).message }));
|
|
84
89
|
clearAttachedFiles();
|
|
85
90
|
}
|
|
86
91
|
} else {
|
|
@@ -92,7 +97,7 @@ export async function sendMessage() {
|
|
|
92
97
|
headers: { 'Content-Type': 'application/json' },
|
|
93
98
|
body: JSON.stringify({ prompt: text }),
|
|
94
99
|
});
|
|
95
|
-
const data = await res.json().catch(() => ({}));
|
|
100
|
+
const data: MessageResult = await res.json().catch(() => ({}));
|
|
96
101
|
if (!res.ok) {
|
|
97
102
|
addSystemMsg(`❌ ${data.error || t('chat.requestFail', { status: res.status })}`, '', 'error');
|
|
98
103
|
return;
|
|
@@ -106,11 +111,11 @@ export async function sendMessage() {
|
|
|
106
111
|
}
|
|
107
112
|
}
|
|
108
113
|
|
|
109
|
-
export function handleKey(e) {
|
|
114
|
+
export function handleKey(e: KeyboardEvent): void {
|
|
110
115
|
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing) { e.preventDefault(); sendMessage(); }
|
|
111
116
|
}
|
|
112
117
|
|
|
113
|
-
async function uploadFile(file) {
|
|
118
|
+
async function uploadFile(file: File): Promise<string> {
|
|
114
119
|
const res = await fetch('/api/upload', {
|
|
115
120
|
method: 'POST',
|
|
116
121
|
headers: { 'X-Filename': encodeURIComponent(file.name) },
|
|
@@ -121,30 +126,31 @@ async function uploadFile(file) {
|
|
|
121
126
|
return data.path;
|
|
122
127
|
}
|
|
123
128
|
|
|
124
|
-
export function attachFiles(files) {
|
|
129
|
+
export function attachFiles(files: File[]): void {
|
|
125
130
|
for (const file of files) {
|
|
126
|
-
// Skip duplicates by name
|
|
127
131
|
if (state.attachedFiles.some(f => f.name === file.name)) continue;
|
|
128
132
|
state.attachedFiles.push(file);
|
|
129
133
|
}
|
|
130
134
|
renderFilePreview();
|
|
131
|
-
document.getElementById('chatInput')
|
|
135
|
+
(document.getElementById('chatInput') as HTMLTextAreaElement | null)?.focus();
|
|
132
136
|
}
|
|
133
137
|
|
|
134
|
-
export function removeAttachedFile(index) {
|
|
138
|
+
export function removeAttachedFile(index: number): void {
|
|
135
139
|
state.attachedFiles.splice(index, 1);
|
|
136
140
|
renderFilePreview();
|
|
137
141
|
}
|
|
138
142
|
|
|
139
|
-
export function clearAttachedFiles() {
|
|
143
|
+
export function clearAttachedFiles(): void {
|
|
140
144
|
state.attachedFiles = [];
|
|
141
145
|
renderFilePreview();
|
|
142
|
-
document.getElementById('fileInput')
|
|
146
|
+
const fi = document.getElementById('fileInput') as HTMLInputElement | null;
|
|
147
|
+
if (fi) fi.value = '';
|
|
143
148
|
}
|
|
144
149
|
|
|
145
|
-
function renderFilePreview() {
|
|
150
|
+
function renderFilePreview(): void {
|
|
146
151
|
const preview = document.getElementById('filePreview');
|
|
147
152
|
const listEl = document.getElementById('filePreviewList');
|
|
153
|
+
if (!preview) return;
|
|
148
154
|
if (!state.attachedFiles.length) {
|
|
149
155
|
preview.classList.remove('visible');
|
|
150
156
|
if (listEl) listEl.innerHTML = '';
|
|
@@ -152,7 +158,7 @@ function renderFilePreview() {
|
|
|
152
158
|
}
|
|
153
159
|
preview.classList.add('visible');
|
|
154
160
|
if (!listEl) return;
|
|
155
|
-
listEl.innerHTML = state.attachedFiles.map((f, i) => {
|
|
161
|
+
listEl.innerHTML = state.attachedFiles.map((f: File, i: number) => {
|
|
156
162
|
const size = (f.size / 1024).toFixed(1);
|
|
157
163
|
const isImg = f.type.startsWith('image/');
|
|
158
164
|
const thumb = isImg ? `<img src="${URL.createObjectURL(f)}" class="file-chip-thumb" alt="">` : '';
|
|
@@ -164,30 +170,32 @@ function renderFilePreview() {
|
|
|
164
170
|
}).join('');
|
|
165
171
|
}
|
|
166
172
|
|
|
167
|
-
export async function clearChat() {
|
|
173
|
+
export async function clearChat(): Promise<void> {
|
|
168
174
|
apiFire('/api/clear', 'POST');
|
|
169
|
-
document.getElementById('chatMessages')
|
|
175
|
+
const chatEl = document.getElementById('chatMessages');
|
|
176
|
+
if (chatEl) chatEl.innerHTML = '';
|
|
170
177
|
}
|
|
171
178
|
|
|
172
179
|
// ── Auto-resize textarea ──
|
|
173
|
-
function autoResize(el) {
|
|
180
|
+
function autoResize(el: HTMLTextAreaElement): void {
|
|
174
181
|
el.style.height = 'auto';
|
|
175
182
|
el.style.height = el.scrollHeight + 'px';
|
|
176
183
|
}
|
|
177
184
|
|
|
178
|
-
export function initAutoResize() {
|
|
179
|
-
const el = document.getElementById('chatInput');
|
|
180
|
-
el.addEventListener('input', () => autoResize(el));
|
|
185
|
+
export function initAutoResize(): void {
|
|
186
|
+
const el = document.getElementById('chatInput') as HTMLTextAreaElement | null;
|
|
187
|
+
if (el) el.addEventListener('input', () => autoResize(el));
|
|
181
188
|
}
|
|
182
189
|
|
|
183
|
-
export function resetInputHeight() {
|
|
184
|
-
const el = document.getElementById('chatInput');
|
|
185
|
-
el.style.height = 'auto';
|
|
190
|
+
export function resetInputHeight(): void {
|
|
191
|
+
const el = document.getElementById('chatInput') as HTMLTextAreaElement | null;
|
|
192
|
+
if (el) el.style.height = 'auto';
|
|
186
193
|
}
|
|
187
194
|
|
|
188
|
-
export function initDragDrop() {
|
|
195
|
+
export function initDragDrop(): void {
|
|
189
196
|
const chatArea = document.querySelector('.chat-area');
|
|
190
197
|
const overlay = document.getElementById('dragOverlay');
|
|
198
|
+
if (!chatArea || !overlay) return;
|
|
191
199
|
let dragCounter = 0;
|
|
192
200
|
|
|
193
201
|
chatArea.addEventListener('dragenter', (e) => {
|
|
@@ -205,26 +213,27 @@ export function initDragDrop() {
|
|
|
205
213
|
e.preventDefault();
|
|
206
214
|
dragCounter = 0;
|
|
207
215
|
overlay.classList.remove('visible');
|
|
208
|
-
const
|
|
216
|
+
const de = e as DragEvent;
|
|
217
|
+
const files = [...(de.dataTransfer?.files || [])];
|
|
209
218
|
if (files.length) attachFiles(files);
|
|
210
219
|
});
|
|
211
220
|
|
|
212
|
-
document.getElementById('fileInput')
|
|
213
|
-
const
|
|
221
|
+
(document.getElementById('fileInput') as HTMLInputElement | null)?.addEventListener('change', (e) => {
|
|
222
|
+
const target = e.target as HTMLInputElement;
|
|
223
|
+
const files = [...(target.files || [])];
|
|
214
224
|
if (files.length) attachFiles(files);
|
|
215
|
-
|
|
225
|
+
target.value = '';
|
|
216
226
|
});
|
|
217
227
|
|
|
218
228
|
// ── Clipboard paste (Cmd+V) ──
|
|
219
|
-
document.addEventListener('paste', (e) => {
|
|
229
|
+
document.addEventListener('paste', (e: ClipboardEvent) => {
|
|
220
230
|
const items = e.clipboardData?.items;
|
|
221
231
|
if (!items) return;
|
|
222
|
-
const files = [];
|
|
232
|
+
const files: File[] = [];
|
|
223
233
|
for (const item of items) {
|
|
224
234
|
if (item.kind !== 'file') continue;
|
|
225
235
|
const blob = item.getAsFile();
|
|
226
236
|
if (!blob) continue;
|
|
227
|
-
// Pasted images often have no useful name; generate one
|
|
228
237
|
if (!blob.name || blob.name === 'image.png') {
|
|
229
238
|
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
230
239
|
const ext = blob.type.split('/')[1] || 'png';
|
|
@@ -1,52 +1,76 @@
|
|
|
1
1
|
// ── Employees Feature ──
|
|
2
2
|
import { state } from '../state.js';
|
|
3
3
|
import { MODEL_MAP, ROLE_PRESETS, getCliKeys } from '../constants.js';
|
|
4
|
+
import type { RolePreset } from '../constants.js';
|
|
4
5
|
import { escapeHtml } from '../render.js';
|
|
5
6
|
import { getAgentPhase } from '../ws.js';
|
|
6
7
|
import { t } from './i18n.js';
|
|
7
8
|
import { api, apiJson, apiFire } from '../api.js';
|
|
8
9
|
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
interface Employee {
|
|
11
|
+
id: string;
|
|
12
|
+
name?: string;
|
|
13
|
+
cli: string;
|
|
14
|
+
model?: string;
|
|
15
|
+
role?: string;
|
|
16
|
+
status?: string;
|
|
17
|
+
phase?: string;
|
|
18
|
+
phaseLabel?: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const LEGACY_MAP: Record<string, string> = {
|
|
22
|
+
'React/Vue 기반 UI 컴포넌트 개발, 스타일링': 'frontend',
|
|
23
|
+
'API 서버, DB 스키마, 비즈니스 로직 구현': 'backend',
|
|
24
|
+
'프론트엔드와 백엔드 모두 담당': 'frontend',
|
|
25
|
+
'CI/CD, Docker, 인프라 자동화': 'backend',
|
|
26
|
+
'테스트 작성, 버그 재현, 품질 관리': 'custom',
|
|
27
|
+
'데이터 파이프라인, ETL, 분석 쿼리': 'data',
|
|
28
|
+
'API 문서화, README, 가이드 작성': 'docs',
|
|
29
|
+
'UI/UX 구현, CSS, 컴포넌트 개발': 'frontend',
|
|
30
|
+
'API, DB, 서버 로직 구현': 'backend',
|
|
31
|
+
'데이터 파이프라인, 분석, ML': 'data',
|
|
32
|
+
'문서화, README, API docs': 'docs',
|
|
33
|
+
'UI/UX, CSS, components': 'frontend',
|
|
34
|
+
'API, DB, server logic': 'backend',
|
|
35
|
+
'Data pipeline, analysis, ML': 'data',
|
|
36
|
+
'Documentation, README, API docs': 'docs',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const PHASE_COLORS: Record<string, string> = {
|
|
40
|
+
'1': '#60a5fa', '2': '#a78bfa', '3': '#34d399', '4': '#fbbf24', '5': '#f472b6'
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export async function loadEmployees(): Promise<void> {
|
|
44
|
+
const data = await api<Employee[]>('/api/employees');
|
|
11
45
|
state.employees = data || [];
|
|
12
46
|
renderEmployees();
|
|
13
47
|
}
|
|
14
48
|
|
|
15
|
-
export function renderEmployees() {
|
|
49
|
+
export function renderEmployees(): void {
|
|
16
50
|
const el = document.getElementById('employeesList');
|
|
17
|
-
if (
|
|
51
|
+
if (!el) return;
|
|
52
|
+
const employees = state.employees as Employee[];
|
|
53
|
+
if (employees.length === 0) {
|
|
18
54
|
el.innerHTML = `<div style="color:var(--text-dim);font-size:11px;padding:4px 0">${t('emp.addPrompt')}</div>`;
|
|
19
55
|
return;
|
|
20
56
|
}
|
|
21
57
|
const cliKeys = getCliKeys();
|
|
22
|
-
el.innerHTML =
|
|
58
|
+
el.innerHTML = employees.map(a => {
|
|
23
59
|
const models = MODEL_MAP[a.cli] || [];
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
'API 서버, DB 스키마, 비즈니스 로직 구현': 'backend',
|
|
29
|
-
'프론트엔드와 백엔드 모두 담당': 'frontend',
|
|
30
|
-
'CI/CD, Docker, 인프라 자동화': 'backend',
|
|
31
|
-
'테스트 작성, 버그 재현, 품질 관리': 'custom',
|
|
32
|
-
'데이터 파이프라인, ETL, 분석 쿼리': 'data',
|
|
33
|
-
'API 문서화, README, 가이드 작성': 'docs',
|
|
34
|
-
// Previous default Korean preset prompts
|
|
35
|
-
'UI/UX 구현, CSS, 컴포넌트 개발': 'frontend',
|
|
36
|
-
'API, DB, 서버 로직 구현': 'backend',
|
|
37
|
-
'데이터 파이프라인, 분석, ML': 'data',
|
|
38
|
-
'문서화, README, API docs': 'docs',
|
|
39
|
-
// Phase 6.9: new English preset prompts
|
|
40
|
-
'UI/UX, CSS, components': 'frontend',
|
|
41
|
-
'API, DB, server logic': 'backend',
|
|
42
|
-
'Data pipeline, analysis, ML': 'data',
|
|
43
|
-
'Documentation, README, API docs': 'docs',
|
|
44
|
-
};
|
|
45
|
-
const legacyVal = LEGACY_MAP[a.role];
|
|
46
|
-
const matched = legacyVal ? ROLE_PRESETS.find(r => r.value === legacyVal) : ROLE_PRESETS.find(r => r.prompt === a.role);
|
|
60
|
+
const legacyVal = LEGACY_MAP[a.role || ''];
|
|
61
|
+
const matched: RolePreset | undefined = legacyVal
|
|
62
|
+
? ROLE_PRESETS.find(r => r.value === legacyVal)
|
|
63
|
+
: ROLE_PRESETS.find(r => r.prompt === a.role);
|
|
47
64
|
const presetVal = matched ? matched.value : (a.role ? 'custom' : 'frontend');
|
|
48
65
|
const isCustom = presetVal === 'custom';
|
|
49
66
|
|
|
67
|
+
const ps = getAgentPhase(a.id);
|
|
68
|
+
const p = ps?.phase || a.phase;
|
|
69
|
+
const pl = ps?.phaseLabel || a.phaseLabel;
|
|
70
|
+
const phaseBadge = p
|
|
71
|
+
? `<span style="background:${PHASE_COLORS[p] || '#888'};color:#000;padding:1px 6px;border-radius:9px;font-size:9px">${pl || 'P' + p}</span>`
|
|
72
|
+
: '';
|
|
73
|
+
|
|
50
74
|
return `
|
|
51
75
|
<div class="settings-group" style="margin-bottom:8px;padding:8px 10px">
|
|
52
76
|
<div style="display:flex;align-items:center;gap:6px;margin-bottom:6px">
|
|
@@ -77,38 +101,43 @@ export function renderEmployees() {
|
|
|
77
101
|
${ROLE_PRESETS.map(r => `<option value="${r.value}"${presetVal === r.value ? ' selected' : ''}>${r.label}</option>`).join('')}
|
|
78
102
|
</select>
|
|
79
103
|
<textarea data-emp-custom="${a.id}" style="display:${isCustom ? 'block' : 'none'};margin-top:4px;width:100%;height:40px;background:var(--bg);border:1px solid var(--border);color:var(--text);padding:4px 6px;border-radius:4px;font-size:10px;font-family:inherit;resize:vertical"
|
|
80
|
-
placeholder="${t('emp.customRole')}">${isCustom ? escapeHtml(a.role) : ''}</textarea>
|
|
104
|
+
placeholder="${t('emp.customRole')}">${isCustom ? escapeHtml(a.role || '') : ''}</textarea>
|
|
81
105
|
</div>
|
|
82
106
|
<div style="margin-top:4px;font-size:10px;display:flex;align-items:center;gap:6px">
|
|
83
107
|
<span style="color:${a.status === 'running' ? '#fbbf24' : 'var(--green)'}">● ${a.status || 'idle'}</span>
|
|
84
|
-
${
|
|
108
|
+
${phaseBadge}
|
|
85
109
|
</div>
|
|
86
110
|
</div>`;
|
|
87
111
|
}).join('');
|
|
88
112
|
}
|
|
89
113
|
|
|
90
|
-
export async function addEmployee() {
|
|
114
|
+
export async function addEmployee(): Promise<void> {
|
|
91
115
|
await apiJson('/api/employees', 'POST', {});
|
|
92
116
|
}
|
|
93
117
|
|
|
94
|
-
export async function updateEmployee(id, data) {
|
|
118
|
+
export async function updateEmployee(id: string, data: Partial<Employee>): Promise<void> {
|
|
95
119
|
await apiJson(`/api/employees/${id}`, 'PUT', data);
|
|
96
120
|
}
|
|
97
121
|
|
|
98
|
-
export async function deleteEmployee(id) {
|
|
122
|
+
export async function deleteEmployee(id: string): Promise<void> {
|
|
99
123
|
apiFire(`/api/employees/${id}`, 'DELETE');
|
|
100
124
|
}
|
|
101
125
|
|
|
102
|
-
export function onEmpCliChange(id, cli) {
|
|
126
|
+
export function onEmpCliChange(id: string, cli: string): void {
|
|
103
127
|
const models = MODEL_MAP[cli] || [];
|
|
104
|
-
const sel = document.querySelector(`[data-emp-model="${id}"]`);
|
|
105
|
-
|
|
128
|
+
const sel = document.querySelector(`[data-emp-model="${id}"]`) as HTMLSelectElement | null;
|
|
129
|
+
if (sel) {
|
|
130
|
+
sel.innerHTML = `<option value="default" selected>default</option>` +
|
|
131
|
+
models.map(m => `<option>${escapeHtml(m)}</option>`).join('') +
|
|
132
|
+
`<option value="__custom__">${t('emp.customModel')}</option>`;
|
|
133
|
+
}
|
|
106
134
|
updateEmployee(id, { cli, model: 'default' });
|
|
107
135
|
}
|
|
108
136
|
|
|
109
|
-
export function onEmpRoleChange(id, presetVal) {
|
|
137
|
+
export function onEmpRoleChange(id: string, presetVal: string): void {
|
|
110
138
|
const preset = ROLE_PRESETS.find(r => r.value === presetVal);
|
|
111
|
-
const customEl = document.querySelector(`[data-emp-custom="${id}"]`);
|
|
139
|
+
const customEl = document.querySelector(`[data-emp-custom="${id}"]`) as HTMLTextAreaElement | null;
|
|
140
|
+
if (!customEl) return;
|
|
112
141
|
if (presetVal === 'custom') {
|
|
113
142
|
customEl.style.display = 'block';
|
|
114
143
|
customEl.focus();
|