claude-controller 0.1.2 → 0.2.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.
@@ -0,0 +1,144 @@
1
+ /* ═══════════════════════════════════════════════
2
+ Attachments — 파일 업로드, 첨부 관리
3
+ ═══════════════════════════════════════════════ */
4
+
5
+ const attachments = [];
6
+
7
+ function updateAttachBadge() {
8
+ document.getElementById('imgCountBadge').textContent = '';
9
+ }
10
+
11
+ function insertAtCursor(textarea, text) {
12
+ const start = textarea.selectionStart;
13
+ const end = textarea.selectionEnd;
14
+ const before = textarea.value.substring(0, start);
15
+ const after = textarea.value.substring(end);
16
+ const space = (before.length > 0 && !before.endsWith(' ') && !before.endsWith('\n')) ? ' ' : '';
17
+ textarea.value = before + space + text + ' ' + after;
18
+ const newPos = start + space.length + text.length + 1;
19
+ textarea.selectionStart = textarea.selectionEnd = newPos;
20
+ textarea.focus();
21
+ updatePromptMirror();
22
+ }
23
+
24
+ function updatePromptMirror() {
25
+ const ta = document.getElementById('promptInput');
26
+ const mirror = document.getElementById('promptMirror');
27
+ if (!ta || !mirror) return;
28
+ const val = ta.value;
29
+ if (!val) {
30
+ mirror.innerHTML = '';
31
+ syncAttachmentsFromText('');
32
+ return;
33
+ }
34
+ const escaped = escapeHtml(val);
35
+ mirror.innerHTML = escaped.replace(/@(\/[^\s,]+|image\d+)/g, (match) => {
36
+ return `<span class="prompt-at-ref">${escapeHtml(match)}</span>`;
37
+ }) + '\n';
38
+ mirror.scrollTop = ta.scrollTop;
39
+ syncAttachmentsFromText(val);
40
+ }
41
+
42
+ function syncAttachmentsFromText(text) {
43
+ const container = document.getElementById('attachmentPreviews');
44
+ if (!container) return;
45
+ let changed = false;
46
+ attachments.forEach((att, idx) => {
47
+ if (!att) return;
48
+ const ref = `@image${idx}`;
49
+ if (!text.includes(ref)) {
50
+ attachments[idx] = null;
51
+ const thumb = container.querySelector(`[data-idx="${idx}"]`);
52
+ if (thumb) thumb.remove();
53
+ changed = true;
54
+ }
55
+ });
56
+ if (changed) updateAttachBadge();
57
+ }
58
+
59
+ async function uploadFile(file) {
60
+ return new Promise((resolve, reject) => {
61
+ const reader = new FileReader();
62
+ reader.onload = async () => {
63
+ try {
64
+ const data = await apiFetch('/api/upload', {
65
+ method: 'POST',
66
+ body: JSON.stringify({ filename: file.name, data: reader.result }),
67
+ });
68
+ resolve(data);
69
+ } catch (err) {
70
+ reject(err);
71
+ }
72
+ };
73
+ reader.onerror = () => reject(new Error(t('msg_file_read_failed')));
74
+ reader.readAsDataURL(file);
75
+ });
76
+ }
77
+
78
+ function removeAttachment(idx) {
79
+ attachments[idx] = null;
80
+ const container = document.getElementById('attachmentPreviews');
81
+ const thumb = container.querySelector(`[data-idx="${idx}"]`);
82
+ if (thumb) thumb.remove();
83
+ const ta = document.getElementById('promptInput');
84
+ ta.value = ta.value.replace(new RegExp(`\\s*@image${idx}\\b`, 'g'), '');
85
+ updateAttachBadge();
86
+ updatePromptMirror();
87
+ }
88
+
89
+ function clearAttachments() {
90
+ attachments.length = 0;
91
+ document.getElementById('attachmentPreviews').innerHTML = '';
92
+ updateAttachBadge();
93
+ updatePromptMirror();
94
+ }
95
+
96
+ function openFilePicker() {
97
+ document.getElementById('filePickerInput').click();
98
+ }
99
+
100
+ async function handleFiles(files) {
101
+ const container = document.getElementById('attachmentPreviews');
102
+ for (const file of files) {
103
+ const isImage = file.type.startsWith('image/');
104
+ const localUrl = isImage ? URL.createObjectURL(file) : null;
105
+
106
+ const tempIdx = attachments.length;
107
+ attachments.push({ localUrl, serverPath: null, filename: file.name, isImage, size: file.size });
108
+
109
+ const thumb = document.createElement('div');
110
+ thumb.dataset.idx = tempIdx;
111
+
112
+ if (isImage) {
113
+ thumb.className = 'img-thumb uploading';
114
+ thumb.innerHTML = `<img src="${localUrl}" alt="${escapeHtml(file.name)}">
115
+ <button class="img-remove" onclick="removeAttachment(${tempIdx})" title="제거">&times;</button>`;
116
+ } else {
117
+ thumb.className = 'file-thumb uploading';
118
+ thumb.innerHTML = `
119
+ <div class="file-icon">${escapeHtml(getFileExt(file.name))}</div>
120
+ <div class="file-info">
121
+ <div class="file-name" title="${escapeHtml(file.name)}">${escapeHtml(file.name)}</div>
122
+ <div class="file-size">${formatFileSize(file.size)}</div>
123
+ </div>
124
+ <button class="file-remove" onclick="removeAttachment(${tempIdx})" title="제거">&times;</button>`;
125
+ }
126
+ container.appendChild(thumb);
127
+ updateAttachBadge();
128
+
129
+ try {
130
+ const data = await uploadFile(file);
131
+ if (!attachments[tempIdx]) continue;
132
+ attachments[tempIdx].serverPath = data.path;
133
+ attachments[tempIdx].filename = data.filename || file.name;
134
+ thumb.classList.remove('uploading');
135
+ const ta = document.getElementById('promptInput');
136
+ insertAtCursor(ta, `@image${tempIdx}`);
137
+ } catch (err) {
138
+ showToast(`${t('msg_upload_failed')}: ${escapeHtml(file.name)} — ${err.message}`, 'error');
139
+ if (attachments[tempIdx]) attachments[tempIdx] = null;
140
+ if (thumb.parentNode) thumb.remove();
141
+ updateAttachBadge();
142
+ }
143
+ }
144
+ }
@@ -0,0 +1,458 @@
1
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
2
+
3
+ :root {
4
+ --bg: #0f1117;
5
+ --surface: #1a1d27;
6
+ --surface-hover: #222531;
7
+ --surface-active: #2a2d3a;
8
+ --border: #2e3140;
9
+ --border-light: #3a3d4a;
10
+ --text: #e4e6ed;
11
+ --text-secondary: #8b8fa3;
12
+ --text-muted: #5c5f73;
13
+ --accent: #6c8cff;
14
+ --accent-hover: #8aa4ff;
15
+ --accent-glow: rgba(108, 140, 255, 0.15);
16
+ --green: #34d399;
17
+ --green-dim: rgba(52, 211, 153, 0.15);
18
+ --red: #f87171;
19
+ --red-dim: rgba(248, 113, 113, 0.15);
20
+ --yellow: #fbbf24;
21
+ --yellow-dim: rgba(251, 191, 36, 0.15);
22
+ --blue: #60a5fa;
23
+ --blue-dim: rgba(96, 165, 250, 0.15);
24
+ --radius: 8px;
25
+ --radius-lg: 12px;
26
+ --font: 'Noto Sans KR', system-ui, -apple-system, sans-serif;
27
+ --font-mono: 'JetBrains Mono', 'SF Mono', 'Consolas', monospace;
28
+ --transition: 0.2s ease;
29
+ --shadow: 0 2px 8px rgba(0,0,0,0.3);
30
+ }
31
+
32
+ html { font-size: 14px; }
33
+
34
+ body {
35
+ font-family: var(--font);
36
+ background: var(--bg);
37
+ color: var(--text);
38
+ min-height: 100vh;
39
+ line-height: 1.6;
40
+ -webkit-font-smoothing: antialiased;
41
+ max-width: 100vw;
42
+ overflow-x: hidden;
43
+ }
44
+
45
+ /* ── Scrollbar ── */
46
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
47
+ ::-webkit-scrollbar-track { background: transparent; }
48
+ ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 3px; }
49
+ ::-webkit-scrollbar-thumb:hover { background: var(--border-light); }
50
+
51
+ /* ── Buttons ── */
52
+ button {
53
+ font-family: var(--font);
54
+ cursor: pointer;
55
+ border: none;
56
+ outline: none;
57
+ transition: all var(--transition);
58
+ }
59
+
60
+ .btn {
61
+ display: inline-flex;
62
+ align-items: center;
63
+ gap: 6px;
64
+ padding: 7px 14px;
65
+ border-radius: var(--radius);
66
+ font-size: 0.82rem;
67
+ font-weight: 500;
68
+ background: var(--surface-hover);
69
+ color: var(--text-secondary);
70
+ border: 1px solid var(--border);
71
+ }
72
+
73
+ .btn:hover {
74
+ background: var(--surface-active);
75
+ color: var(--text);
76
+ border-color: var(--border-light);
77
+ }
78
+
79
+ .btn:active { transform: scale(0.97); }
80
+
81
+ .btn-primary {
82
+ background: var(--accent);
83
+ color: #fff;
84
+ border-color: var(--accent);
85
+ }
86
+
87
+ .btn-primary:hover {
88
+ background: var(--accent-hover);
89
+ border-color: var(--accent-hover);
90
+ box-shadow: 0 0 20px var(--accent-glow);
91
+ }
92
+
93
+ .btn-danger {
94
+ color: var(--red);
95
+ }
96
+
97
+ .btn-danger:hover {
98
+ background: var(--red-dim);
99
+ border-color: var(--red);
100
+ }
101
+
102
+ .btn-warning {
103
+ color: var(--yellow);
104
+ }
105
+
106
+ .btn-warning:hover {
107
+ background: var(--yellow-dim);
108
+ border-color: var(--yellow);
109
+ }
110
+
111
+ .btn-success {
112
+ color: var(--green);
113
+ }
114
+
115
+ .btn-success:hover {
116
+ background: var(--green-dim);
117
+ border-color: var(--green);
118
+ }
119
+
120
+ .btn-sm {
121
+ padding: 4px 10px;
122
+ font-size: 0.75rem;
123
+ }
124
+
125
+ .btn-icon {
126
+ padding: 7px;
127
+ display: inline-flex;
128
+ align-items: center;
129
+ justify-content: center;
130
+ }
131
+
132
+ .btn-collapse svg {
133
+ transition: transform 0.2s ease;
134
+ }
135
+ .btn-collapse.collapsed svg {
136
+ transform: rotate(-90deg);
137
+ }
138
+
139
+ .table-wrap.collapsed {
140
+ display: none;
141
+ }
142
+
143
+ /* ── Context Toolbar ── */
144
+ .ctx-toolbar-wrap {
145
+ position: relative;
146
+ flex-shrink: 0;
147
+ }
148
+
149
+ .ctx-toolbar {
150
+ display: flex;
151
+ align-items: center;
152
+ gap: 2px;
153
+ flex-shrink: 0;
154
+ }
155
+
156
+ .ctx-btn {
157
+ padding: 4px 10px;
158
+ border-radius: var(--radius);
159
+ font-size: 0.72rem;
160
+ font-weight: 600;
161
+ font-family: var(--font-mono);
162
+ background: transparent;
163
+ color: var(--text-muted);
164
+ border: 1px solid transparent;
165
+ letter-spacing: 0.02em;
166
+ text-transform: lowercase;
167
+ }
168
+
169
+ .ctx-btn:hover {
170
+ background: var(--surface-hover);
171
+ color: var(--text-secondary);
172
+ border-color: var(--border);
173
+ }
174
+
175
+ .ctx-btn.active {
176
+ background: var(--accent-glow);
177
+ color: var(--accent);
178
+ border-color: var(--accent);
179
+ }
180
+
181
+ .ctx-btn-x {
182
+ padding: 4px 6px;
183
+ border-radius: var(--radius);
184
+ font-size: 0.78rem;
185
+ font-weight: 500;
186
+ background: transparent;
187
+ color: var(--text-muted);
188
+ border: 1px solid transparent;
189
+ line-height: 1;
190
+ display: inline-flex;
191
+ align-items: center;
192
+ justify-content: center;
193
+ }
194
+
195
+ .ctx-btn-x:hover {
196
+ background: var(--red-dim);
197
+ color: var(--red);
198
+ border-color: var(--red);
199
+ }
200
+
201
+ .ctx-session-label {
202
+ display: none;
203
+ padding: 2px 8px;
204
+ border-radius: 12px;
205
+ font-size: 0.7rem;
206
+ font-family: var(--font-mono);
207
+ background: var(--blue-dim);
208
+ color: var(--blue);
209
+ overflow: hidden;
210
+ text-overflow: ellipsis;
211
+ white-space: nowrap;
212
+ line-height: 1.6;
213
+ min-width: 0;
214
+ }
215
+
216
+ .ctx-session-label.visible {
217
+ display: inline;
218
+ }
219
+
220
+ .session-picker {
221
+ display: none;
222
+ flex-direction: column;
223
+ position: absolute;
224
+ top: 100%;
225
+ right: 0;
226
+ margin-top: 4px;
227
+ width: 520px;
228
+ max-width: calc(100vw - 32px);
229
+ max-height: 420px;
230
+ background: var(--surface);
231
+ border: 1px solid var(--border);
232
+ border-radius: var(--radius-lg);
233
+ box-shadow: 0 8px 32px rgba(0,0,0,0.4);
234
+ z-index: 200;
235
+ }
236
+
237
+ .session-picker.open { display: flex; }
238
+
239
+ .session-picker-header {
240
+ display: flex;
241
+ align-items: center;
242
+ justify-content: space-between;
243
+ padding: 10px 14px;
244
+ border-bottom: 1px solid var(--border);
245
+ flex-shrink: 0;
246
+ }
247
+
248
+ .session-picker-title {
249
+ font-size: 0.78rem;
250
+ font-weight: 600;
251
+ color: var(--text-secondary);
252
+ }
253
+
254
+ .session-picker-search {
255
+ padding: 8px 14px;
256
+ border-bottom: 1px solid var(--border);
257
+ flex-shrink: 0;
258
+ }
259
+
260
+ .session-picker-search input {
261
+ width: 100%;
262
+ padding: 6px 10px;
263
+ border: 1px solid var(--border);
264
+ border-radius: var(--radius);
265
+ background: var(--bg);
266
+ color: var(--text);
267
+ font-size: 0.78rem;
268
+ font-family: var(--font);
269
+ outline: none;
270
+ transition: border-color var(--transition);
271
+ }
272
+
273
+ .session-picker-search input:focus {
274
+ border-color: var(--accent);
275
+ }
276
+
277
+ .session-picker-search input::placeholder {
278
+ color: var(--text-muted);
279
+ }
280
+
281
+ .session-picker-list {
282
+ overflow-y: auto;
283
+ flex: 1;
284
+ }
285
+
286
+ .session-item {
287
+ display: flex;
288
+ flex-direction: column;
289
+ gap: 4px;
290
+ padding: 10px 14px;
291
+ cursor: pointer;
292
+ border-bottom: 1px solid var(--border);
293
+ transition: background var(--transition);
294
+ }
295
+
296
+ .session-item:hover { background: var(--surface-hover); }
297
+ .session-item:last-child { border-bottom: none; }
298
+
299
+ .session-item-row {
300
+ display: flex;
301
+ align-items: center;
302
+ gap: 8px;
303
+ }
304
+
305
+ .session-item-status {
306
+ display: inline-block;
307
+ width: 7px;
308
+ height: 7px;
309
+ border-radius: 50%;
310
+ flex-shrink: 0;
311
+ }
312
+ .session-item-status.done { background: var(--green); }
313
+ .session-item-status.failed { background: var(--red); }
314
+ .session-item-status.running { background: var(--yellow); animation: pulse 1.5s infinite; }
315
+ .session-item-status.unknown { background: var(--text-muted); }
316
+
317
+ @keyframes pulse {
318
+ 0%, 100% { opacity: 1; }
319
+ 50% { opacity: 0.4; }
320
+ }
321
+
322
+ .session-item-id {
323
+ font-family: var(--font-mono);
324
+ font-size: 0.7rem;
325
+ color: var(--accent);
326
+ flex-shrink: 0;
327
+ }
328
+
329
+ .session-item-slug {
330
+ font-size: 0.7rem;
331
+ color: var(--text-muted);
332
+ flex-shrink: 0;
333
+ font-style: italic;
334
+ }
335
+
336
+ .session-item-job {
337
+ font-family: var(--font-mono);
338
+ font-size: 0.65rem;
339
+ color: var(--text-muted);
340
+ flex-shrink: 0;
341
+ }
342
+
343
+ .session-item-prompt {
344
+ font-size: 0.78rem;
345
+ color: var(--text);
346
+ flex: 1;
347
+ overflow: hidden;
348
+ text-overflow: ellipsis;
349
+ white-space: nowrap;
350
+ }
351
+
352
+ .session-item-meta {
353
+ display: flex;
354
+ align-items: center;
355
+ gap: 12px;
356
+ padding-left: 15px;
357
+ }
358
+
359
+ .session-item-time {
360
+ font-size: 0.68rem;
361
+ color: var(--text-muted);
362
+ flex-shrink: 0;
363
+ }
364
+
365
+ .session-item-cwd {
366
+ font-size: 0.65rem;
367
+ color: var(--text-muted);
368
+ font-family: var(--font-mono);
369
+ overflow: hidden;
370
+ text-overflow: ellipsis;
371
+ white-space: nowrap;
372
+ }
373
+
374
+ .session-item-cost {
375
+ font-size: 0.65rem;
376
+ color: var(--green);
377
+ font-family: var(--font-mono);
378
+ flex-shrink: 0;
379
+ }
380
+
381
+ .session-filter-bar {
382
+ display: flex;
383
+ align-items: center;
384
+ gap: 6px;
385
+ padding: 6px 14px;
386
+ border-bottom: 1px solid var(--border);
387
+ flex-shrink: 0;
388
+ }
389
+
390
+ .session-filter-toggle {
391
+ display: inline-flex;
392
+ align-items: center;
393
+ gap: 4px;
394
+ padding: 3px 8px;
395
+ border: 1px solid var(--border);
396
+ border-radius: 4px;
397
+ background: transparent;
398
+ color: var(--text-muted);
399
+ font-size: 0.68rem;
400
+ font-family: var(--font);
401
+ cursor: pointer;
402
+ transition: all var(--transition);
403
+ white-space: nowrap;
404
+ }
405
+
406
+ .session-filter-toggle:hover { border-color: var(--accent); color: var(--text-secondary); }
407
+ .session-filter-toggle.active { background: var(--accent-glow); border-color: var(--accent); color: var(--accent); }
408
+
409
+ .session-filter-project {
410
+ font-size: 0.68rem;
411
+ color: var(--text-muted);
412
+ overflow: hidden;
413
+ text-overflow: ellipsis;
414
+ white-space: nowrap;
415
+ flex: 1;
416
+ }
417
+
418
+ .session-group-header {
419
+ display: flex;
420
+ align-items: center;
421
+ gap: 6px;
422
+ padding: 6px 14px;
423
+ background: var(--bg);
424
+ font-size: 0.68rem;
425
+ font-weight: 600;
426
+ color: var(--text-secondary);
427
+ border-bottom: 1px solid var(--border);
428
+ position: sticky;
429
+ top: 0;
430
+ z-index: 1;
431
+ }
432
+
433
+ .session-group-header svg {
434
+ width: 12px;
435
+ height: 12px;
436
+ flex-shrink: 0;
437
+ opacity: 0.6;
438
+ }
439
+
440
+ .session-group-count {
441
+ font-weight: 400;
442
+ color: var(--text-muted);
443
+ margin-left: auto;
444
+ }
445
+
446
+ .session-empty {
447
+ padding: 24px 14px;
448
+ text-align: center;
449
+ font-size: 0.78rem;
450
+ color: var(--text-muted);
451
+ }
452
+
453
+ .session-empty-icon {
454
+ font-size: 1.5rem;
455
+ margin-bottom: 8px;
456
+ opacity: 0.5;
457
+ }
458
+