claude-controller 0.2.0 → 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 (68) hide show
  1. package/README.md +2 -2
  2. package/bin/autoloop.sh +382 -0
  3. package/bin/ctl +327 -5
  4. package/bin/native-app.py +5 -2
  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 +5 -1
  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 +464 -26
  38. package/web/handler_fs.py +15 -14
  39. package/web/handler_goals.py +203 -0
  40. package/web/handler_jobs.py +165 -42
  41. package/web/handler_memory.py +203 -0
  42. package/web/jobs.py +576 -12
  43. package/web/personas.py +419 -0
  44. package/web/pipeline.py +682 -50
  45. package/web/presets.py +506 -0
  46. package/web/projects.py +58 -4
  47. package/web/static/api.js +90 -3
  48. package/web/static/app.js +8 -0
  49. package/web/static/base.css +51 -12
  50. package/web/static/context.js +14 -4
  51. package/web/static/form.css +3 -2
  52. package/web/static/goals.css +363 -0
  53. package/web/static/goals.js +300 -0
  54. package/web/static/i18n.js +288 -0
  55. package/web/static/index.html +142 -6
  56. package/web/static/jobs.css +951 -4
  57. package/web/static/jobs.js +890 -54
  58. package/web/static/memoryview.js +117 -0
  59. package/web/static/personas.js +228 -0
  60. package/web/static/pipeline.css +308 -1
  61. package/web/static/pipelines.js +249 -14
  62. package/web/static/presets.js +244 -0
  63. package/web/static/send.js +26 -4
  64. package/web/static/settings-style.css +34 -3
  65. package/web/static/settings.js +37 -1
  66. package/web/static/stream.js +242 -19
  67. package/web/static/utils.js +54 -2
  68. package/web/webhook.py +210 -0
package/web/static/api.js CHANGED
@@ -8,15 +8,58 @@ let AUTH_TOKEN = '';
8
8
  let _backendConnected = false;
9
9
  let serviceRunning = null;
10
10
 
11
+ /* ── Connection health tracking ── */
12
+ let _connFailCount = 0;
13
+ const _CONN_FAIL_THRESHOLD = 3;
14
+ let _connBannerVisible = false;
15
+
16
+ function _updateConnBanner(ok) {
17
+ if (ok) {
18
+ if (_connFailCount > 0) _connFailCount = 0;
19
+ if (_connBannerVisible) {
20
+ _connBannerVisible = false;
21
+ const banner = document.getElementById('connLostBanner');
22
+ if (banner) banner.classList.remove('visible');
23
+ }
24
+ } else {
25
+ _connFailCount++;
26
+ if (_connFailCount >= _CONN_FAIL_THRESHOLD && !_connBannerVisible) {
27
+ _connBannerVisible = true;
28
+ const banner = document.getElementById('connLostBanner');
29
+ if (banner) banner.classList.add('visible');
30
+ }
31
+ }
32
+ }
33
+
11
34
  async function apiFetch(path, options = {}) {
12
35
  const headers = { 'Content-Type': 'application/json', ...options.headers };
13
36
  if (AUTH_TOKEN) {
14
37
  headers['Authorization'] = `Bearer ${AUTH_TOKEN}`;
15
38
  }
16
- const resp = await fetch(`${API}${path}`, { ...options, headers });
39
+ let resp;
40
+ try {
41
+ resp = await fetch(`${API}${path}`, { ...options, headers });
42
+ } catch (networkErr) {
43
+ _updateConnBanner(false);
44
+ throw networkErr;
45
+ }
46
+ _updateConnBanner(true);
17
47
  if (!resp.ok) {
18
- const text = await resp.text().catch(() => '');
19
- throw new Error(text || `HTTP ${resp.status}`);
48
+ let msg = '';
49
+ try {
50
+ const ct = resp.headers.get('content-type') || '';
51
+ if (ct.includes('application/json')) {
52
+ const body = await resp.json();
53
+ if (body.error && typeof body.error === 'object') {
54
+ msg = body.error.message || JSON.stringify(body.error);
55
+ } else {
56
+ msg = body.error || body.message || JSON.stringify(body);
57
+ }
58
+ } else {
59
+ msg = await resp.text();
60
+ }
61
+ } catch { /* parse fail — fall through */ }
62
+ throw new Error(msg || `HTTP ${resp.status}`);
20
63
  }
21
64
  const ct = resp.headers.get('content-type') || '';
22
65
  if (ct.includes('application/json')) return resp.json();
@@ -33,6 +76,50 @@ async function checkStatus() {
33
76
  }
34
77
  }
35
78
 
79
+ /* ── Goals API ── */
80
+ async function fetchGoals(status) {
81
+ const qs = status ? `?status=${status}` : '';
82
+ return apiFetch(`/api/goals${qs}`);
83
+ }
84
+ async function createGoal(objective, mode = 'gate', opts = {}) {
85
+ return apiFetch('/api/goals', {
86
+ method: 'POST',
87
+ body: JSON.stringify({ objective, mode, ...opts }),
88
+ });
89
+ }
90
+ async function getGoal(id) { return apiFetch(`/api/goals/${id}`); }
91
+ async function updateGoal(id, fields) {
92
+ return apiFetch(`/api/goals/${id}/update`, {
93
+ method: 'POST', body: JSON.stringify(fields),
94
+ });
95
+ }
96
+ async function approveGoal(id) {
97
+ return apiFetch(`/api/goals/${id}/approve`, { method: 'POST' });
98
+ }
99
+ async function cancelGoal(id) {
100
+ return apiFetch(`/api/goals/${id}`, { method: 'DELETE' });
101
+ }
102
+
103
+ /* ── Memory API ── */
104
+ async function fetchMemories(params = {}) {
105
+ const qs = new URLSearchParams(params).toString();
106
+ return apiFetch(`/api/memory${qs ? '?' + qs : ''}`);
107
+ }
108
+ async function createMemory(data) {
109
+ return apiFetch('/api/memory', {
110
+ method: 'POST', body: JSON.stringify(data),
111
+ });
112
+ }
113
+ async function getMemory(id) { return apiFetch(`/api/memory/${id}`); }
114
+ async function updateMemory(id, fields) {
115
+ return apiFetch(`/api/memory/${id}/update`, {
116
+ method: 'POST', body: JSON.stringify(fields),
117
+ });
118
+ }
119
+ async function deleteMemory(id) {
120
+ return apiFetch(`/api/memory/${id}`, { method: 'DELETE' });
121
+ }
122
+
36
123
  async function serviceAction(action) {
37
124
  const btn = document.getElementById(`btn${action.charAt(0).toUpperCase() + action.slice(1)}`);
38
125
  if (btn) btn.disabled = true;
package/web/static/app.js CHANGED
@@ -37,13 +37,21 @@ async function autoConnect() {
37
37
 
38
38
  async function init() {
39
39
  applyI18n();
40
+ applyTheme(localStorage.getItem('theme') || 'dark');
40
41
  await autoConnect();
41
42
  loadRecentDirs();
43
+ fetchPersonas();
42
44
  fetchPipelines();
43
45
  checkStatus();
46
+ fetchRegisteredProjects();
44
47
  fetchJobs();
48
+ fetchStats();
49
+ _applyJobListCollapse();
50
+ requestNotificationPermission();
45
51
 
46
52
  jobPollTimer = setInterval(fetchJobs, 3000);
53
+ setInterval(fetchStats, 15000);
54
+ setInterval(fetchRegisteredProjects, 30000);
47
55
  setInterval(checkStatus, 10000);
48
56
 
49
57
  const promptInput = document.getElementById('promptInput');
@@ -29,6 +29,32 @@
29
29
  --shadow: 0 2px 8px rgba(0,0,0,0.3);
30
30
  }
31
31
 
32
+ /* ── Light Theme ── */
33
+ [data-theme="light"] {
34
+ --bg: #f5f6fa;
35
+ --surface: #ffffff;
36
+ --surface-hover: #f0f1f5;
37
+ --surface-active: #e8e9ef;
38
+ --border: #d8dae3;
39
+ --border-light: #c5c8d4;
40
+ --text: #1a1d27;
41
+ --text-secondary: #4a4e63;
42
+ --text-muted: #8b8fa3;
43
+ --accent: #4f6cf7;
44
+ --accent-hover: #3d5ce5;
45
+ --accent-glow: rgba(79, 108, 247, 0.12);
46
+ --green: #0d9668;
47
+ --green-dim: rgba(13, 150, 104, 0.1);
48
+ --red: #dc3545;
49
+ --red-dim: rgba(220, 53, 69, 0.08);
50
+ --yellow: #c07d10;
51
+ --yellow-dim: rgba(192, 125, 16, 0.08);
52
+ --blue: #2563eb;
53
+ --blue-dim: rgba(37, 99, 235, 0.08);
54
+ --shadow: 0 2px 8px rgba(0,0,0,0.08);
55
+ --stream-bg: #f0f1f5;
56
+ }
57
+
32
58
  html { font-size: 14px; }
33
59
 
34
60
  body {
@@ -164,6 +190,7 @@ button {
164
190
  border: 1px solid transparent;
165
191
  letter-spacing: 0.02em;
166
192
  text-transform: lowercase;
193
+ transition: all 0.15s ease;
167
194
  }
168
195
 
169
196
  .ctx-btn:hover {
@@ -178,6 +205,14 @@ button {
178
205
  border-color: var(--accent);
179
206
  }
180
207
 
208
+ .ctx-btn.active[id="ctxResume"],
209
+ .ctx-btn.active[id="ctxFork"] {
210
+ background: var(--blue);
211
+ color: #fff;
212
+ border-color: var(--blue);
213
+ box-shadow: 0 1px 4px rgba(0,0,0,0.15);
214
+ }
215
+
181
216
  .ctx-btn-x {
182
217
  padding: 4px 6px;
183
218
  border-radius: var(--radius);
@@ -200,21 +235,32 @@ button {
200
235
 
201
236
  .ctx-session-label {
202
237
  display: none;
203
- padding: 2px 8px;
238
+ padding: 3px 10px;
204
239
  border-radius: 12px;
205
- font-size: 0.7rem;
240
+ font-size: 0.75rem;
241
+ font-weight: 600;
206
242
  font-family: var(--font-mono);
207
- background: var(--blue-dim);
208
- color: var(--blue);
243
+ background: var(--blue);
244
+ color: #fff;
209
245
  overflow: hidden;
210
246
  text-overflow: ellipsis;
211
247
  white-space: nowrap;
212
248
  line-height: 1.6;
213
249
  min-width: 0;
250
+ max-width: 200px;
251
+ box-shadow: 0 0 0 2px var(--blue-dim), 0 1px 4px rgba(0,0,0,0.15);
252
+ animation: ctx-label-pop 0.2s ease-out;
253
+ }
254
+
255
+ @keyframes ctx-label-pop {
256
+ from { opacity: 0; transform: scale(0.9); }
257
+ to { opacity: 1; transform: scale(1); }
214
258
  }
215
259
 
216
260
  .ctx-session-label.visible {
217
- display: inline;
261
+ display: inline-flex;
262
+ align-items: center;
263
+ gap: 4px;
218
264
  }
219
265
 
220
266
  .session-picker {
@@ -371,13 +417,6 @@ button {
371
417
  white-space: nowrap;
372
418
  }
373
419
 
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
420
  .session-filter-bar {
382
421
  display: flex;
383
422
  align-items: center;
@@ -24,19 +24,29 @@ function _updateContextUI() {
24
24
  const promptInfo = document.getElementById('promptSessionInfo');
25
25
 
26
26
  [newBtn, resumeBtn, forkBtn].forEach(b => b.classList.remove('active'));
27
+ label.classList.remove('visible');
28
+ label.textContent = '';
29
+
27
30
  if (_contextMode === 'new') {
28
31
  newBtn.classList.add('active');
29
- label.textContent = '';
30
- if (promptInfo) promptInfo.textContent = 'new session';
32
+ if (promptInfo) promptInfo.textContent = '';
31
33
  } else if (_contextMode === 'resume') {
32
34
  resumeBtn.classList.add('active');
33
35
  const sid = _contextSessionId ? _contextSessionId.slice(0, 8) : '';
34
- label.textContent = sid ? `resume:${sid}…` : '';
36
+ if (sid) {
37
+ const promptSnippet = _contextSessionPrompt ? _contextSessionPrompt.slice(0, 24) : '';
38
+ label.textContent = promptSnippet ? `${sid}… ${promptSnippet}` : `${sid}…`;
39
+ label.classList.add('visible');
40
+ }
35
41
  if (promptInfo) promptInfo.textContent = sid ? `resume:${sid}` : 'resume';
36
42
  } else if (_contextMode === 'fork') {
37
43
  forkBtn.classList.add('active');
38
44
  const sid = _contextSessionId ? _contextSessionId.slice(0, 8) : '';
39
- label.textContent = sid ? `fork:${sid}…` : '';
45
+ if (sid) {
46
+ const promptSnippet = _contextSessionPrompt ? _contextSessionPrompt.slice(0, 24) : '';
47
+ label.textContent = promptSnippet ? `${sid}… ${promptSnippet}` : `${sid}…`;
48
+ label.classList.add('visible');
49
+ }
40
50
  if (promptInfo) promptInfo.textContent = sid ? `fork:${sid}` : 'fork';
41
51
  }
42
52
  }
@@ -522,8 +522,8 @@ textarea { min-height: unset; position: relative; }
522
522
  }
523
523
 
524
524
  .prompt-wrapper.drag-over textarea {
525
- border-color: var(--accent);
526
- box-shadow: 0 0 0 3px var(--accent-glow);
525
+ border-color: transparent;
526
+ box-shadow: none;
527
527
  }
528
528
 
529
529
  .drop-overlay {
@@ -545,6 +545,7 @@ textarea { min-height: unset; position: relative; }
545
545
 
546
546
  .prompt-wrapper.drag-over .drop-overlay {
547
547
  display: flex;
548
+ box-shadow: 0 0 0 3px var(--accent-glow);
548
549
  }
549
550
 
550
551
  .image-previews {
@@ -0,0 +1,363 @@
1
+ /* ═══════════════════════════════════════════════
2
+ Goals — 목표 관리 + DAG 시각화 스타일
3
+ ═══════════════════════════════════════════════ */
4
+
5
+ /* ── Goal Filter Bar ── */
6
+ .goal-filter-bar {
7
+ display: flex;
8
+ align-items: center;
9
+ gap: 8px;
10
+ padding: 8px 16px;
11
+ border-bottom: 1px solid var(--border);
12
+ }
13
+ .goal-filter-btns {
14
+ display: flex;
15
+ gap: 2px;
16
+ }
17
+ .goal-filter-btn {
18
+ padding: 4px 12px;
19
+ font-size: 0.72rem;
20
+ border: none;
21
+ background: transparent;
22
+ color: var(--text-muted);
23
+ border-radius: 4px;
24
+ cursor: pointer;
25
+ transition: all var(--transition);
26
+ }
27
+ .goal-filter-btn:hover { background: var(--surface-hover); color: var(--text-secondary); }
28
+ .goal-filter-btn.active { background: var(--accent-glow); color: var(--accent); font-weight: 500; }
29
+
30
+ /* ── Goal List ── */
31
+ .goal-list {
32
+ display: flex;
33
+ flex-direction: column;
34
+ }
35
+
36
+ /* ── Goal Card ── */
37
+ .goal-card {
38
+ padding: 14px 16px;
39
+ border-bottom: 1px solid var(--border);
40
+ cursor: pointer;
41
+ transition: background var(--transition);
42
+ }
43
+ .goal-card:hover { background: var(--surface-hover); }
44
+ .goal-card:last-child { border-bottom: none; }
45
+ .goal-card.expanded { background: var(--surface-hover); }
46
+
47
+ .goal-card-header {
48
+ display: flex;
49
+ align-items: flex-start;
50
+ gap: 10px;
51
+ }
52
+ .goal-card-status {
53
+ flex-shrink: 0;
54
+ width: 8px;
55
+ height: 8px;
56
+ border-radius: 50%;
57
+ margin-top: 5px;
58
+ }
59
+ .goal-card-status.pending { background: var(--text-muted); }
60
+ .goal-card-status.planning { background: var(--yellow); animation: pulse-glow 2s infinite; }
61
+ .goal-card-status.ready { background: var(--blue); }
62
+ .goal-card-status.running { background: var(--blue); animation: pulse-glow 1.5s infinite; }
63
+ .goal-card-status.gate_waiting { background: var(--yellow); }
64
+ .goal-card-status.evaluating { background: var(--yellow); animation: pulse-glow 2s infinite; }
65
+ .goal-card-status.completed { background: var(--green); }
66
+ .goal-card-status.failed { background: var(--red); }
67
+ .goal-card-status.cancelled { background: var(--text-muted); opacity: 0.5; }
68
+
69
+ @keyframes pulse-glow {
70
+ 0%, 100% { opacity: 1; }
71
+ 50% { opacity: 0.4; }
72
+ }
73
+
74
+ .goal-card-body { flex: 1; min-width: 0; }
75
+ .goal-card-objective {
76
+ font-size: 0.82rem;
77
+ font-weight: 500;
78
+ color: var(--text);
79
+ line-height: 1.4;
80
+ margin-bottom: 4px;
81
+ }
82
+ .goal-card-meta {
83
+ display: flex;
84
+ align-items: center;
85
+ gap: 12px;
86
+ font-size: 0.68rem;
87
+ color: var(--text-muted);
88
+ }
89
+ .goal-card-meta .goal-mode-badge {
90
+ padding: 1px 6px;
91
+ border-radius: 3px;
92
+ background: var(--accent-glow);
93
+ color: var(--accent);
94
+ font-weight: 500;
95
+ font-size: 0.65rem;
96
+ text-transform: uppercase;
97
+ }
98
+ .goal-card-meta .goal-status-label {
99
+ font-weight: 500;
100
+ }
101
+ .goal-card-actions {
102
+ display: flex;
103
+ gap: 4px;
104
+ flex-shrink: 0;
105
+ }
106
+ .goal-card-actions .btn {
107
+ padding: 3px 8px;
108
+ font-size: 0.68rem;
109
+ }
110
+
111
+ /* ── Goal Progress Bar ── */
112
+ .goal-progress-bar {
113
+ height: 3px;
114
+ background: var(--border);
115
+ border-radius: 2px;
116
+ margin-top: 8px;
117
+ overflow: hidden;
118
+ }
119
+ .goal-progress-fill {
120
+ height: 100%;
121
+ background: var(--accent);
122
+ border-radius: 2px;
123
+ transition: width 0.5s ease;
124
+ }
125
+ .goal-progress-fill.done { background: var(--green); }
126
+ .goal-progress-fill.failed { background: var(--red); }
127
+
128
+ /* ── Goal Detail (expanded) ── */
129
+ .goal-detail {
130
+ padding: 12px 16px 16px 34px;
131
+ display: none;
132
+ }
133
+ .goal-card.expanded .goal-detail { display: block; }
134
+
135
+ /* ── DAG Tree View ── */
136
+ .dag-tree {
137
+ margin-top: 8px;
138
+ }
139
+ .dag-tree ul {
140
+ list-style: none;
141
+ padding-left: 20px;
142
+ position: relative;
143
+ }
144
+ .dag-tree > ul { padding-left: 0; }
145
+
146
+ .dag-tree li {
147
+ position: relative;
148
+ padding: 4px 0 4px 18px;
149
+ }
150
+ .dag-tree li::before {
151
+ content: '';
152
+ position: absolute;
153
+ left: 0;
154
+ top: 0;
155
+ bottom: 0;
156
+ width: 1px;
157
+ background: var(--border-light);
158
+ }
159
+ .dag-tree li::after {
160
+ content: '';
161
+ position: absolute;
162
+ left: 0;
163
+ top: 14px;
164
+ width: 12px;
165
+ height: 1px;
166
+ background: var(--border-light);
167
+ }
168
+ .dag-tree li:last-child::before { height: 14px; }
169
+
170
+ .dag-node {
171
+ display: inline-flex;
172
+ align-items: center;
173
+ gap: 6px;
174
+ padding: 4px 10px;
175
+ border-radius: 4px;
176
+ font-size: 0.72rem;
177
+ background: var(--surface);
178
+ border: 1px solid var(--border);
179
+ transition: all var(--transition);
180
+ }
181
+ .dag-node:hover { border-color: var(--accent); }
182
+
183
+ .dag-node-dot {
184
+ width: 6px;
185
+ height: 6px;
186
+ border-radius: 50%;
187
+ flex-shrink: 0;
188
+ }
189
+ .dag-node-dot.pending { background: var(--text-muted); }
190
+ .dag-node-dot.running { background: var(--blue); animation: pulse-glow 1.5s infinite; }
191
+ .dag-node-dot.completed { background: var(--green); }
192
+ .dag-node-dot.failed { background: var(--red); }
193
+
194
+ .dag-node-label { color: var(--text); }
195
+ .dag-node-type {
196
+ font-size: 0.62rem;
197
+ color: var(--text-muted);
198
+ font-family: var(--font-mono, monospace);
199
+ }
200
+
201
+ /* ── Goal Create Form ── */
202
+ .goal-create-form {
203
+ padding: 12px 16px;
204
+ border-bottom: 1px solid var(--border);
205
+ display: none;
206
+ }
207
+ .goal-create-form.visible { display: block; }
208
+ .goal-create-row {
209
+ display: flex;
210
+ gap: 8px;
211
+ align-items: flex-end;
212
+ margin-bottom: 8px;
213
+ }
214
+ .goal-create-row .form-field { flex: 1; }
215
+ .goal-create-row .form-field label {
216
+ display: block;
217
+ font-size: 0.68rem;
218
+ color: var(--text-secondary);
219
+ margin-bottom: 3px;
220
+ }
221
+ .goal-create-row .form-field input,
222
+ .goal-create-row .form-field select,
223
+ .goal-create-row .form-field textarea {
224
+ width: 100%;
225
+ padding: 6px 10px;
226
+ font-size: 0.78rem;
227
+ background: var(--bg);
228
+ border: 1px solid var(--border);
229
+ border-radius: var(--radius);
230
+ color: var(--text);
231
+ font-family: var(--font);
232
+ }
233
+ .goal-create-row .form-field textarea {
234
+ resize: vertical;
235
+ min-height: 36px;
236
+ }
237
+ .goal-create-actions {
238
+ display: flex;
239
+ justify-content: flex-end;
240
+ gap: 6px;
241
+ }
242
+
243
+ /* ── Memory Section ── */
244
+ .memory-filter-bar {
245
+ display: flex;
246
+ align-items: center;
247
+ gap: 8px;
248
+ padding: 8px 16px;
249
+ border-bottom: 1px solid var(--border);
250
+ }
251
+ .memory-search-wrap {
252
+ flex: 1;
253
+ display: flex;
254
+ align-items: center;
255
+ gap: 6px;
256
+ }
257
+ .memory-search-wrap input {
258
+ flex: 1;
259
+ padding: 5px 10px;
260
+ font-size: 0.75rem;
261
+ background: var(--bg);
262
+ border: 1px solid var(--border);
263
+ border-radius: var(--radius);
264
+ color: var(--text);
265
+ }
266
+ .memory-type-select {
267
+ padding: 5px 8px;
268
+ font-size: 0.72rem;
269
+ background: var(--bg);
270
+ border: 1px solid var(--border);
271
+ border-radius: var(--radius);
272
+ color: var(--text);
273
+ }
274
+
275
+ /* ── Memory Card ── */
276
+ .memory-list {
277
+ display: flex;
278
+ flex-direction: column;
279
+ }
280
+ .memory-card {
281
+ padding: 12px 16px;
282
+ border-bottom: 1px solid var(--border);
283
+ transition: background var(--transition);
284
+ }
285
+ .memory-card:hover { background: var(--surface-hover); }
286
+ .memory-card:last-child { border-bottom: none; }
287
+
288
+ .memory-card-header {
289
+ display: flex;
290
+ align-items: center;
291
+ gap: 8px;
292
+ margin-bottom: 4px;
293
+ }
294
+ .memory-type-badge {
295
+ padding: 1px 6px;
296
+ border-radius: 3px;
297
+ font-size: 0.62rem;
298
+ font-weight: 600;
299
+ text-transform: uppercase;
300
+ }
301
+ .memory-type-badge.decision { background: var(--blue-dim); color: var(--blue); }
302
+ .memory-type-badge.pattern { background: var(--green-dim); color: var(--green); }
303
+ .memory-type-badge.failure { background: var(--red-dim); color: var(--red); }
304
+ .memory-type-badge.context { background: var(--yellow-dim); color: var(--yellow); }
305
+
306
+ .memory-card-title {
307
+ font-size: 0.78rem;
308
+ font-weight: 500;
309
+ color: var(--text);
310
+ flex: 1;
311
+ }
312
+ .memory-card-actions {
313
+ display: flex;
314
+ gap: 4px;
315
+ }
316
+ .memory-card-actions button {
317
+ padding: 2px 6px;
318
+ background: transparent;
319
+ border: none;
320
+ color: var(--text-muted);
321
+ cursor: pointer;
322
+ border-radius: 3px;
323
+ transition: all var(--transition);
324
+ }
325
+ .memory-card-actions button:hover { background: var(--surface-active); color: var(--text); }
326
+
327
+ .memory-card-content {
328
+ font-size: 0.72rem;
329
+ color: var(--text-secondary);
330
+ line-height: 1.5;
331
+ margin-bottom: 4px;
332
+ white-space: pre-wrap;
333
+ max-height: 60px;
334
+ overflow: hidden;
335
+ }
336
+ .memory-card-tags {
337
+ display: flex;
338
+ gap: 4px;
339
+ flex-wrap: wrap;
340
+ }
341
+ .memory-tag {
342
+ padding: 1px 6px;
343
+ font-size: 0.62rem;
344
+ background: var(--surface-active);
345
+ color: var(--text-muted);
346
+ border-radius: 3px;
347
+ }
348
+
349
+ /* ── Memory Create Form ── */
350
+ .memory-create-form {
351
+ padding: 12px 16px;
352
+ border-bottom: 1px solid var(--border);
353
+ display: none;
354
+ }
355
+ .memory-create-form.visible { display: block; }
356
+
357
+ /* ── Empty State ── */
358
+ .goal-empty, .memory-empty {
359
+ padding: 32px 20px;
360
+ text-align: center;
361
+ color: var(--text-muted);
362
+ font-size: 0.8rem;
363
+ }