claude-controller 0.1.2 → 0.3.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.
Files changed (71) hide show
  1. package/README.md +2 -2
  2. package/bin/autoloop.sh +382 -0
  3. package/bin/ctl +1189 -0
  4. package/bin/native-app.py +6 -3
  5. package/bin/watchdog.sh +357 -0
  6. package/cognitive/__init__.py +14 -0
  7. package/cognitive/__pycache__/__init__.cpython-314.pyc +0 -0
  8. package/cognitive/__pycache__/dispatcher.cpython-314.pyc +0 -0
  9. package/cognitive/__pycache__/evaluator.cpython-314.pyc +0 -0
  10. package/cognitive/__pycache__/goal_engine.cpython-314.pyc +0 -0
  11. package/cognitive/__pycache__/learning.cpython-314.pyc +0 -0
  12. package/cognitive/__pycache__/orchestrator.cpython-314.pyc +0 -0
  13. package/cognitive/__pycache__/planner.cpython-314.pyc +0 -0
  14. package/cognitive/dispatcher.py +192 -0
  15. package/cognitive/evaluator.py +289 -0
  16. package/cognitive/goal_engine.py +232 -0
  17. package/cognitive/learning.py +189 -0
  18. package/cognitive/orchestrator.py +303 -0
  19. package/cognitive/planner.py +207 -0
  20. package/cognitive/prompts/analyst.md +31 -0
  21. package/cognitive/prompts/coder.md +22 -0
  22. package/cognitive/prompts/reviewer.md +33 -0
  23. package/cognitive/prompts/tester.md +21 -0
  24. package/cognitive/prompts/writer.md +25 -0
  25. package/config.sh +6 -1
  26. package/dag/__init__.py +5 -0
  27. package/dag/__pycache__/__init__.cpython-314.pyc +0 -0
  28. package/dag/__pycache__/graph.cpython-314.pyc +0 -0
  29. package/dag/graph.py +222 -0
  30. package/lib/jobs.sh +12 -1
  31. package/package.json +11 -5
  32. package/postinstall.sh +1 -1
  33. package/service/controller.sh +43 -11
  34. package/web/audit.py +122 -0
  35. package/web/checkpoint.py +80 -0
  36. package/web/config.py +2 -5
  37. package/web/handler.py +634 -473
  38. package/web/handler_fs.py +153 -0
  39. package/web/handler_goals.py +203 -0
  40. package/web/handler_jobs.py +372 -0
  41. package/web/handler_memory.py +203 -0
  42. package/web/handler_sessions.py +132 -0
  43. package/web/jobs.py +585 -13
  44. package/web/personas.py +419 -0
  45. package/web/pipeline.py +981 -0
  46. package/web/presets.py +506 -0
  47. package/web/projects.py +246 -0
  48. package/web/static/api.js +141 -0
  49. package/web/static/app.js +25 -1937
  50. package/web/static/attachments.js +144 -0
  51. package/web/static/base.css +497 -0
  52. package/web/static/context.js +204 -0
  53. package/web/static/dirs.js +246 -0
  54. package/web/static/form.css +763 -0
  55. package/web/static/goals.css +363 -0
  56. package/web/static/goals.js +300 -0
  57. package/web/static/i18n.js +625 -0
  58. package/web/static/index.html +215 -13
  59. package/web/static/{styles.css → jobs.css} +746 -1141
  60. package/web/static/jobs.js +1270 -0
  61. package/web/static/memoryview.js +117 -0
  62. package/web/static/personas.js +228 -0
  63. package/web/static/pipeline.css +338 -0
  64. package/web/static/pipelines.js +487 -0
  65. package/web/static/presets.js +244 -0
  66. package/web/static/send.js +135 -0
  67. package/web/static/settings-style.css +291 -0
  68. package/web/static/settings.js +81 -0
  69. package/web/static/stream.js +534 -0
  70. package/web/static/utils.js +131 -0
  71. package/web/webhook.py +210 -0
@@ -0,0 +1,244 @@
1
+ /* ===================================================
2
+ Presets -- 자동화 프리셋 브라우징/적용 UI
3
+ =================================================== */
4
+
5
+ let _presets = [];
6
+ let _presetsPanelOpen = false;
7
+
8
+ const _PRESET_ICONS = {
9
+ rocket: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z"/><path d="M12 15l-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z"/><path d="M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0"/><path d="M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5"/></svg>',
10
+ search: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>',
11
+ book: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>',
12
+ shield: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>',
13
+ layers: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="12 2 2 7 12 12 22 7 12 2"/><polyline points="2 17 12 22 22 17"/><polyline points="2 12 12 17 22 12"/></svg>',
14
+ bookmark: '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z"/></svg>',
15
+ };
16
+
17
+ async function fetchPresets() {
18
+ try {
19
+ _presets = await apiFetch('/api/presets');
20
+ renderPresetCards();
21
+ } catch { /* silent */ }
22
+ }
23
+
24
+ function renderPresetCards() {
25
+ const container = document.getElementById('presetGrid');
26
+ if (!container) return;
27
+
28
+ if (_presets.length === 0) {
29
+ container.innerHTML = '<div class="empty-state" style="padding:20px;text-align:center;color:var(--text-muted);font-size:0.8rem;">프리셋이 없습니다.</div>';
30
+ return;
31
+ }
32
+
33
+ container.innerHTML = _presets.map(p => {
34
+ const icon = _PRESET_ICONS[p.icon] || _PRESET_ICONS.layers;
35
+ const pipeNames = (p.pipeline_names || []).map(n => escapeHtml(n)).join(' &rarr; ');
36
+ const builtinBadge = p.builtin
37
+ ? '<span class="preset-badge preset-badge-builtin">내장</span>'
38
+ : '<span class="preset-badge preset-badge-custom">커스텀</span>';
39
+
40
+ return `<div class="preset-card" data-preset-id="${escapeHtml(p.id)}">
41
+ <div class="preset-card-icon">${icon}</div>
42
+ <div class="preset-card-body">
43
+ <div class="preset-card-header">
44
+ <span class="preset-card-name">${escapeHtml(p.name)}</span>
45
+ ${builtinBadge}
46
+ </div>
47
+ <div class="preset-card-desc">${escapeHtml(p.description)}</div>
48
+ <div class="preset-card-pipes">${pipeNames} <span class="preset-pipe-count">(${p.pipeline_count}개)</span></div>
49
+ </div>
50
+ <div class="preset-card-actions">
51
+ <button class="btn btn-sm btn-primary" onclick="openApplyPreset('${escapeHtml(p.id)}')">적용</button>
52
+ ${!p.builtin ? `<button class="btn btn-sm btn-danger" onclick="deletePreset('${escapeHtml(p.id)}')">삭제</button>` : ''}
53
+ </div>
54
+ </div>`;
55
+ }).join('');
56
+ }
57
+
58
+ function openApplyPreset(presetId) {
59
+ const preset = _presets.find(p => p.id === presetId);
60
+ if (!preset) return;
61
+
62
+ const overlayId = 'presetApply_' + Date.now();
63
+ const overlay = document.createElement('div');
64
+ overlay.className = 'settings-overlay';
65
+ overlay.style.cssText = 'display:flex;align-items:center;justify-content:center;';
66
+ overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };
67
+
68
+ const icon = _PRESET_ICONS[preset.icon] || _PRESET_ICONS.layers;
69
+
70
+ overlay.innerHTML = `<div class="settings-panel" style="max-width:480px;margin:0;">
71
+ <div class="settings-header">
72
+ <div class="settings-title" style="display:flex;align-items:center;gap:8px;">
73
+ ${icon}
74
+ <span>프리셋 적용: ${escapeHtml(preset.name)}</span>
75
+ </div>
76
+ <button class="settings-close" onclick="this.closest('.settings-overlay').remove()">
77
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
78
+ </button>
79
+ </div>
80
+ <div class="settings-body">
81
+ <div style="margin-bottom:12px;font-size:0.78rem;color:var(--text-secondary);">
82
+ ${escapeHtml(preset.description)}
83
+ </div>
84
+ <div style="margin-bottom:12px;font-size:0.72rem;color:var(--text-muted);display:flex;gap:6px;flex-wrap:wrap;">
85
+ ${(preset.pipeline_names || []).map(n => `<span class="preset-pipe-tag">${escapeHtml(n)}</span>`).join('')}
86
+ </div>
87
+ <div style="margin-bottom:12px;">
88
+ <label style="font-size:0.72rem;font-weight:600;color:var(--text-secondary);display:block;margin-bottom:4px;">프로젝트 경로</label>
89
+ <div style="display:flex;gap:6px;">
90
+ <input id="${overlayId}_path" type="text" placeholder="/path/to/project"
91
+ value="${escapeHtml(document.getElementById('cwdInput')?.value || '')}"
92
+ style="flex:1;padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-secondary);color:var(--text-primary);font-size:0.8rem;font-family:var(--font-mono,monospace);">
93
+ <button class="btn btn-sm" onclick="_browseForPreset('${overlayId}_path')" title="디렉토리 탐색">
94
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z"/></svg>
95
+ </button>
96
+ </div>
97
+ </div>
98
+ </div>
99
+ <div class="settings-footer" style="display:flex;gap:6px;">
100
+ <button class="btn btn-sm btn-primary" onclick="_doApplyPreset('${escapeHtml(presetId)}','${overlayId}',this)">적용하기</button>
101
+ <button class="btn btn-sm" onclick="this.closest('.settings-overlay').remove()">취소</button>
102
+ </div>
103
+ </div>`;
104
+ document.body.appendChild(overlay);
105
+ }
106
+
107
+ function _browseForPreset(inputId) {
108
+ // 기존 디렉토리 브라우저를 재활용: 선택 후 해당 input에 값 설정
109
+ const input = document.getElementById(inputId);
110
+ if (!input) return;
111
+ // 임시로 cwdInput에 연결하여 기존 dir browser 활용
112
+ const origCwd = document.getElementById('cwdInput')?.value || '';
113
+ toggleDirBrowser();
114
+ // dirBrowser 선택 시 cwdInput에 값이 설정되므로 polling으로 감지
115
+ const poll = setInterval(() => {
116
+ const newVal = document.getElementById('cwdInput')?.value || '';
117
+ if (newVal && newVal !== origCwd) {
118
+ input.value = newVal;
119
+ clearInterval(poll);
120
+ }
121
+ }, 300);
122
+ setTimeout(() => clearInterval(poll), 30000);
123
+ }
124
+
125
+ async function _doApplyPreset(presetId, overlayId, btn) {
126
+ const path = document.getElementById(overlayId + '_path').value.trim();
127
+ if (!path) { showToast('프로젝트 경로를 입력하세요', 'error'); return; }
128
+
129
+ btn.disabled = true;
130
+ btn.textContent = '적용 중...';
131
+ try {
132
+ const result = await apiFetch('/api/presets/apply', {
133
+ method: 'POST',
134
+ headers: { 'Content-Type': 'application/json' },
135
+ body: JSON.stringify({ preset_id: presetId, project_path: path }),
136
+ });
137
+ showToast(`프리셋 "${result.preset_name}" 적용 완료 (${result.pipelines_created}개 파이프라인 생성)`);
138
+ btn.closest('.settings-overlay').remove();
139
+ // 생성된 파이프라인 중 첫 번째를 즉시 실행 (나머지는 체이닝 또는 타이머로 자동 시작)
140
+ if (result.pipelines && result.pipelines.length > 0) {
141
+ runPipeline(result.pipelines[0].id);
142
+ }
143
+ fetchPipelines();
144
+ } catch (err) {
145
+ showToast(`적용 실패: ${err.message}`, 'error');
146
+ btn.disabled = false;
147
+ btn.textContent = '적용하기';
148
+ }
149
+ }
150
+
151
+ async function deletePreset(presetId) {
152
+ if (!confirm('이 프리셋을 삭제하시겠습니까?')) return;
153
+ try {
154
+ await apiFetch(`/api/presets/${encodeURIComponent(presetId)}`, { method: 'DELETE' });
155
+ showToast('프리셋 삭제됨');
156
+ fetchPresets();
157
+ } catch (err) {
158
+ showToast(err.message || '삭제 실패', 'error');
159
+ }
160
+ }
161
+
162
+ /* -- 현재 파이프라인을 프리셋으로 저장 -- */
163
+
164
+ function openSavePresetDialog() {
165
+ if (_pipelines.length === 0) {
166
+ showToast('저장할 파이프라인이 없습니다', 'error');
167
+ return;
168
+ }
169
+ const dlgId = 'presetSave_' + Date.now();
170
+ const overlay = document.createElement('div');
171
+ overlay.className = 'settings-overlay';
172
+ overlay.style.cssText = 'display:flex;align-items:center;justify-content:center;';
173
+ overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); };
174
+
175
+ const pipeChecks = _pipelines.map(p =>
176
+ `<label style="display:flex;gap:6px;align-items:center;font-size:0.78rem;color:var(--text-primary);">
177
+ <input type="checkbox" value="${escapeHtml(p.id)}" checked style="accent-color:var(--primary);">
178
+ ${escapeHtml(p.name || p.id)}
179
+ <span style="font-size:0.65rem;color:var(--text-muted);">${escapeHtml(p.interval || '1회')}</span>
180
+ </label>`
181
+ ).join('');
182
+
183
+ overlay.innerHTML = `<div class="settings-panel" style="max-width:440px;margin:0;">
184
+ <div class="settings-header">
185
+ <div class="settings-title" style="display:flex;align-items:center;gap:8px;">
186
+ ${_PRESET_ICONS.bookmark}
187
+ <span>프리셋으로 저장</span>
188
+ </div>
189
+ <button class="settings-close" onclick="this.closest('.settings-overlay').remove()">
190
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>
191
+ </button>
192
+ </div>
193
+ <div class="settings-body">
194
+ <div style="margin-bottom:12px;">
195
+ <label style="font-size:0.72rem;font-weight:600;color:var(--text-secondary);display:block;margin-bottom:4px;">프리셋 이름</label>
196
+ <input id="${dlgId}_name" type="text" placeholder="나의 자동화 프리셋"
197
+ style="width:100%;padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-secondary);color:var(--text-primary);font-size:0.8rem;box-sizing:border-box;">
198
+ </div>
199
+ <div style="margin-bottom:12px;">
200
+ <label style="font-size:0.72rem;font-weight:600;color:var(--text-secondary);display:block;margin-bottom:4px;">설명</label>
201
+ <input id="${dlgId}_desc" type="text" placeholder="이 프리셋의 용도를 설명하세요"
202
+ style="width:100%;padding:6px 10px;border:1px solid var(--border);border-radius:6px;background:var(--bg-secondary);color:var(--text-primary);font-size:0.8rem;box-sizing:border-box;">
203
+ </div>
204
+ <div style="margin-bottom:8px;">
205
+ <label style="font-size:0.72rem;font-weight:600;color:var(--text-secondary);display:block;margin-bottom:6px;">포함할 파이프라인</label>
206
+ <div id="${dlgId}_pipes" style="display:flex;flex-direction:column;gap:6px;max-height:200px;overflow-y:auto;">
207
+ ${pipeChecks}
208
+ </div>
209
+ </div>
210
+ </div>
211
+ <div class="settings-footer" style="display:flex;gap:6px;">
212
+ <button class="btn btn-sm btn-primary" onclick="_doSavePreset('${dlgId}',this)">저장</button>
213
+ <button class="btn btn-sm" onclick="this.closest('.settings-overlay').remove()">취소</button>
214
+ </div>
215
+ </div>`;
216
+ document.body.appendChild(overlay);
217
+ }
218
+
219
+ async function _doSavePreset(dlgId, btn) {
220
+ const name = document.getElementById(dlgId + '_name').value.trim();
221
+ if (!name) { showToast('프리셋 이름을 입력하세요', 'error'); return; }
222
+
223
+ const desc = document.getElementById(dlgId + '_desc').value.trim();
224
+ const checks = document.querySelectorAll(`#${dlgId}_pipes input[type="checkbox"]:checked`);
225
+ const pipeIds = Array.from(checks).map(c => c.value);
226
+ if (pipeIds.length === 0) { showToast('최소 1개 파이프라인을 선택하세요', 'error'); return; }
227
+
228
+ btn.disabled = true;
229
+ btn.textContent = '저장 중...';
230
+ try {
231
+ const result = await apiFetch('/api/presets/save', {
232
+ method: 'POST',
233
+ headers: { 'Content-Type': 'application/json' },
234
+ body: JSON.stringify({ name, description: desc, pipeline_ids: pipeIds }),
235
+ });
236
+ showToast(`프리셋 "${result.name}" 저장됨 (${result.pipeline_count}개 파이프라인)`);
237
+ btn.closest('.settings-overlay').remove();
238
+ fetchPresets();
239
+ } catch (err) {
240
+ showToast(`저장 실패: ${err.message}`, 'error');
241
+ btn.disabled = false;
242
+ btn.textContent = '저장';
243
+ }
244
+ }
@@ -0,0 +1,135 @@
1
+ /* ═══════════════════════════════════════════════
2
+ Send Task — 작업 전송 및 자동화 토글
3
+ ═══════════════════════════════════════════════ */
4
+
5
+ let _sendLock = false;
6
+ let _automationMode = false;
7
+ let _depsMode = false;
8
+
9
+ function toggleAutomation() {
10
+ _automationMode = !_automationMode;
11
+ const row = document.getElementById('automationRow');
12
+ const btn = document.getElementById('btnAutoToggle');
13
+ const sendBtn = document.getElementById('btnSend');
14
+ if (_automationMode) {
15
+ row.style.display = 'flex';
16
+ btn.style.cssText = 'border-color:var(--accent);color:var(--accent);background:rgba(99,102,241,0.1);';
17
+ sendBtn.querySelector('span').textContent = '자동화 등록';
18
+ } else {
19
+ row.style.display = 'none';
20
+ btn.style.cssText = '';
21
+ sendBtn.querySelector('span').textContent = t('send');
22
+ document.getElementById('automationInterval').value = '';
23
+ }
24
+ }
25
+
26
+ function toggleDeps() {
27
+ // DAG UI는 숨김 처리됨 — AI가 API depends_on으로 직접 사용
28
+ _depsMode = !_depsMode;
29
+ const row = document.getElementById('depsRow');
30
+ if (row) row.style.display = _depsMode ? 'flex' : 'none';
31
+ if (!_depsMode) {
32
+ const inp = document.getElementById('depsInput');
33
+ if (inp) inp.value = '';
34
+ }
35
+ }
36
+
37
+ function clearPromptForm() {
38
+ document.getElementById('promptInput').value = '';
39
+ clearAttachments();
40
+ updatePromptMirror();
41
+ clearDirSelection();
42
+ if (_automationMode) toggleAutomation();
43
+ if (_depsMode) toggleDeps();
44
+ }
45
+
46
+ async function sendTask(e) {
47
+ e.preventDefault();
48
+ if (_sendLock) return false;
49
+
50
+ const prompt = document.getElementById('promptInput').value.trim();
51
+ if (!prompt) {
52
+ showToast(t('msg_prompt_required'), 'error');
53
+ return false;
54
+ }
55
+
56
+ // ── 자동화 모드: 파이프라인 생성 ──
57
+ if (_automationMode) {
58
+ const cwd = document.getElementById('cwdInput').value.trim();
59
+ if (!cwd) { showToast('디렉토리를 선택하세요', 'error'); return false; }
60
+ const interval = document.getElementById('automationInterval').value.trim();
61
+ _sendLock = true;
62
+ const btn = document.getElementById('btnSend');
63
+ btn.disabled = true;
64
+ try {
65
+ const pipe = await apiFetch('/api/pipelines', {
66
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
67
+ body: JSON.stringify({ project_path: cwd, command: prompt, interval }),
68
+ });
69
+ showToast(`자동화 등록: ${pipe.name || pipe.id}`);
70
+ clearPromptForm();
71
+ fetchPipelines();
72
+ runPipeline(pipe.id);
73
+ } catch (err) {
74
+ showToast(`등록 실패: ${err.message}`, 'error');
75
+ } finally {
76
+ _sendLock = false;
77
+ btn.disabled = false;
78
+ btn.querySelector('span').textContent = t('send');
79
+ }
80
+ return false;
81
+ }
82
+
83
+ // ── 일반 전송 모드 ──
84
+ _sendLock = true;
85
+ const cwd = document.getElementById('cwdInput').value.trim();
86
+ const btn = document.getElementById('btnSend');
87
+ btn.disabled = true;
88
+ btn.innerHTML = '<span class="spinner"></span> 전송 중...';
89
+
90
+ try {
91
+ let finalPrompt = prompt;
92
+ const filePaths = [];
93
+ attachments.forEach((att, idx) => {
94
+ if (att && att.serverPath) {
95
+ finalPrompt = finalPrompt.replace(new RegExp(`@image${idx}\\b`, 'g'), `@${att.serverPath}`);
96
+ filePaths.push(att.serverPath);
97
+ }
98
+ });
99
+
100
+ const body = { prompt: finalPrompt };
101
+ if (cwd) body.cwd = cwd;
102
+
103
+ if (_contextSessionId && (_contextMode === 'resume' || _contextMode === 'fork')) {
104
+ body.session = _contextMode + ':' + _contextSessionId;
105
+ }
106
+
107
+ if (filePaths.length > 0) body.images = filePaths;
108
+
109
+ // 의존성 모드: depends_on 추가
110
+ if (_depsMode) {
111
+ const depsVal = document.getElementById('depsInput').value.trim();
112
+ if (depsVal) {
113
+ body.depends_on = depsVal.split(/[,\s]+/).filter(Boolean);
114
+ }
115
+ }
116
+
117
+ const result = await apiFetch('/api/send', { method: 'POST', body: JSON.stringify(body) });
118
+ const modeMsg = _contextMode === 'resume' ? ' (resume)' : _contextMode === 'fork' ? ' (fork)' : '';
119
+ const depMsg = result && result.status === 'pending' ? ' (대기 중 — 선행 작업 완료 후 실행)' : '';
120
+ showToast(t('msg_task_sent') + modeMsg + depMsg);
121
+ if (cwd) addRecentDir(cwd);
122
+ document.getElementById('promptInput').value = '';
123
+ clearAttachments();
124
+ clearContext();
125
+ if (_depsMode) toggleDeps();
126
+ fetchJobs();
127
+ } catch (err) {
128
+ showToast(`${t('msg_send_failed')}: ${err.message}`, 'error');
129
+ } finally {
130
+ _sendLock = false;
131
+ btn.disabled = false;
132
+ btn.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg> <span data-i18n="send">' + t('send') + '</span>';
133
+ }
134
+ return false;
135
+ }
@@ -0,0 +1,291 @@
1
+ /* ── Settings & Overlays ── */
2
+
3
+ /* ── Theme Toggle FAB ── */
4
+ .theme-fab {
5
+ position: fixed;
6
+ bottom: 80px;
7
+ right: 24px;
8
+ z-index: 300;
9
+ width: 36px;
10
+ height: 36px;
11
+ border-radius: 50%;
12
+ background: var(--surface);
13
+ border: 1px solid var(--border);
14
+ color: var(--text-secondary);
15
+ display: flex;
16
+ align-items: center;
17
+ justify-content: center;
18
+ cursor: pointer;
19
+ box-shadow: 0 2px 8px rgba(0,0,0,0.2);
20
+ transition: all var(--transition);
21
+ }
22
+ .theme-fab:hover {
23
+ background: var(--surface-hover);
24
+ color: var(--accent);
25
+ border-color: var(--accent);
26
+ }
27
+ /* dark mode: show sun icon, hide moon */
28
+ .theme-icon-light { display: none; }
29
+ .theme-icon-dark { display: block; }
30
+ [data-theme="light"] .theme-icon-light { display: block; }
31
+ [data-theme="light"] .theme-icon-dark { display: none; }
32
+
33
+ /* ── Settings Panel ── */
34
+ .settings-fab {
35
+ position: fixed;
36
+ bottom: 24px;
37
+ right: 24px;
38
+ z-index: 300;
39
+ width: 44px;
40
+ height: 44px;
41
+ border-radius: 50%;
42
+ background: var(--surface);
43
+ border: 1px solid var(--border);
44
+ color: var(--text-secondary);
45
+ display: flex;
46
+ align-items: center;
47
+ justify-content: center;
48
+ cursor: pointer;
49
+ box-shadow: 0 4px 16px rgba(0,0,0,0.4);
50
+ transition: all var(--transition);
51
+ }
52
+ .settings-fab:hover {
53
+ background: var(--surface-hover);
54
+ color: var(--accent);
55
+ border-color: var(--accent);
56
+ transform: rotate(30deg);
57
+ }
58
+
59
+ .settings-overlay {
60
+ display: none;
61
+ position: fixed;
62
+ inset: 0;
63
+ z-index: 400;
64
+ background: rgba(0,0,0,0.5);
65
+ backdrop-filter: blur(4px);
66
+ animation: fadeIn 0.2s ease;
67
+ }
68
+ .settings-overlay.open { display: flex; align-items: center; justify-content: center; }
69
+
70
+ .settings-panel {
71
+ width: 520px;
72
+ max-width: 90vw;
73
+ max-height: 85vh;
74
+ background: var(--surface);
75
+ border: 1px solid var(--border);
76
+ border-radius: var(--radius-lg);
77
+ box-shadow: 0 16px 48px rgba(0,0,0,0.5);
78
+ display: flex;
79
+ flex-direction: column;
80
+ overflow: hidden;
81
+ animation: panel-slide 0.2s ease-out;
82
+ }
83
+
84
+ .settings-header {
85
+ display: flex;
86
+ align-items: center;
87
+ justify-content: space-between;
88
+ padding: 16px 20px;
89
+ border-bottom: 1px solid var(--border);
90
+ }
91
+ .settings-title {
92
+ font-size: 1rem;
93
+ font-weight: 700;
94
+ color: var(--text);
95
+ display: flex;
96
+ align-items: center;
97
+ gap: 8px;
98
+ }
99
+ .settings-title svg { width: 18px; height: 18px; color: var(--accent); }
100
+ .settings-close {
101
+ padding: 6px;
102
+ border-radius: var(--radius);
103
+ background: transparent;
104
+ color: var(--text-muted);
105
+ border: none;
106
+ cursor: pointer;
107
+ }
108
+ .settings-close:hover { background: var(--surface-active); color: var(--text); }
109
+
110
+ .settings-body {
111
+ flex: 1;
112
+ overflow-y: auto;
113
+ padding: 20px;
114
+ }
115
+
116
+ .settings-section {
117
+ margin-bottom: 20px;
118
+ }
119
+ .settings-section-title {
120
+ font-size: 0.72rem;
121
+ font-weight: 600;
122
+ color: var(--text-muted);
123
+ text-transform: uppercase;
124
+ letter-spacing: 0.06em;
125
+ margin-bottom: 12px;
126
+ }
127
+
128
+ .setting-row {
129
+ display: flex;
130
+ align-items: flex-start;
131
+ justify-content: space-between;
132
+ padding: 10px 0;
133
+ border-bottom: 1px solid var(--border);
134
+ gap: 16px;
135
+ }
136
+ .setting-row:last-child { border-bottom: none; }
137
+
138
+ .setting-info { flex: 1; min-width: 0; }
139
+ .setting-label {
140
+ font-size: 0.85rem;
141
+ font-weight: 500;
142
+ color: var(--text);
143
+ margin-bottom: 2px;
144
+ }
145
+ .setting-desc {
146
+ font-size: 0.72rem;
147
+ color: var(--text-muted);
148
+ line-height: 1.4;
149
+ }
150
+
151
+ .setting-control { flex-shrink: 0; }
152
+
153
+ /* Toggle Switch */
154
+ .toggle {
155
+ position: relative;
156
+ width: 40px;
157
+ height: 22px;
158
+ cursor: pointer;
159
+ }
160
+ .toggle input { display: none; }
161
+ .toggle-track {
162
+ position: absolute;
163
+ inset: 0;
164
+ background: var(--border);
165
+ border-radius: 11px;
166
+ transition: background var(--transition);
167
+ }
168
+ .toggle input:checked + .toggle-track { background: var(--accent); }
169
+ .toggle-thumb {
170
+ position: absolute;
171
+ top: 2px;
172
+ left: 2px;
173
+ width: 18px;
174
+ height: 18px;
175
+ border-radius: 50%;
176
+ background: #fff;
177
+ transition: transform var(--transition);
178
+ box-shadow: 0 1px 3px rgba(0,0,0,0.3);
179
+ }
180
+ .toggle input:checked ~ .toggle-thumb { transform: translateX(18px); }
181
+
182
+ .setting-input {
183
+ width: 100%;
184
+ padding: 7px 10px;
185
+ background: var(--bg);
186
+ border: 1px solid var(--border);
187
+ border-radius: var(--radius);
188
+ color: var(--text);
189
+ font-family: var(--font-mono);
190
+ font-size: 0.8rem;
191
+ transition: border-color var(--transition);
192
+ }
193
+ .setting-input:focus {
194
+ outline: none;
195
+ border-color: var(--accent);
196
+ box-shadow: 0 0 0 3px var(--accent-glow);
197
+ }
198
+ .setting-input-sm { width: 80px; text-align: center; }
199
+
200
+ .settings-footer {
201
+ display: flex;
202
+ align-items: center;
203
+ justify-content: flex-end;
204
+ gap: 8px;
205
+ padding: 14px 20px;
206
+ border-top: 1px solid var(--border);
207
+ }
208
+
209
+ .setting-restart-hint {
210
+ font-size: 0.7rem;
211
+ color: var(--yellow);
212
+ margin-right: auto;
213
+ display: flex;
214
+ align-items: center;
215
+ gap: 4px;
216
+ }
217
+
218
+ /* ── Connection Modal ── */
219
+ .conn-message {
220
+ font-size: 0.8rem;
221
+ color: var(--red);
222
+ background: var(--red-dim);
223
+ border: 1px solid rgba(248, 113, 113, 0.25);
224
+ border-radius: var(--radius);
225
+ padding: 8px 12px;
226
+ }
227
+
228
+ /* ── Running Job Preview Row ── */
229
+ .preview-row {
230
+ cursor: default !important;
231
+ }
232
+ .preview-row,
233
+ .preview-row:hover {
234
+ background: transparent !important;
235
+ }
236
+ .preview-row td {
237
+ padding: 0 !important;
238
+ border-bottom: 1px solid var(--border) !important;
239
+ }
240
+
241
+ .job-preview {
242
+ padding: 6px 14px;
243
+ background: var(--stream-bg, #080a0e);
244
+ font-family: var(--font-mono);
245
+ font-size: 0.78rem;
246
+ line-height: 1.7;
247
+ color: var(--text);
248
+ max-height: calc(0.78rem * 1.7 * 2 + 6px + 6px);
249
+ overflow: hidden;
250
+ box-sizing: content-box;
251
+ }
252
+ .preview-lines {
253
+ min-width: 0;
254
+ overflow: hidden;
255
+ }
256
+ .preview-line {
257
+ padding: 2px 0;
258
+ word-break: break-word;
259
+ color: var(--text);
260
+ }
261
+ .preview-line + .preview-line {
262
+ margin-top: 1px;
263
+ }
264
+ .preview-tool {
265
+ display: inline-flex;
266
+ align-items: center;
267
+ gap: 4px;
268
+ padding: 2px 8px;
269
+ border-radius: 4px;
270
+ background: var(--yellow-dim);
271
+ color: var(--yellow);
272
+ font-size: 0.7rem;
273
+ font-weight: 600;
274
+ white-space: nowrap;
275
+ margin-right: 4px;
276
+ }
277
+ .preview-tool::before {
278
+ content: '>';
279
+ opacity: 0.5;
280
+ }
281
+ .preview-result {
282
+ color: var(--green);
283
+ }
284
+ .preview-result::before {
285
+ content: '✓ ';
286
+ font-weight: 700;
287
+ }
288
+ .preview-text {
289
+ color: var(--text);
290
+ }
291
+