ltcai 4.7.0 → 4.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -43,6 +43,14 @@ export function readProductFlowComplete() {
43
43
  return false;
44
44
  }
45
45
 
46
+ function readSavedFlowUser(): { email?: string; name?: string } | null {
47
+ try {
48
+ const raw = localStorage.getItem(FLOW_USER_KEY);
49
+ return raw ? JSON.parse(raw) : null;
50
+ } catch {}
51
+ return null;
52
+ }
53
+
46
54
  export function ProductFlow({ onComplete }: { onComplete: () => void }) {
47
55
  const [step, setStep] = React.useState<FlowStep>("login");
48
56
  const [analysis, setAnalysis] = React.useState<FlowAnalysis | null>(null);
@@ -135,51 +143,67 @@ export function ProductFlow({ onComplete }: { onComplete: () => void }) {
135
143
 
136
144
  function LoginScreen({ onSuccess }: { onSuccess: () => void }) {
137
145
  const [email, setEmail] = React.useState(() => {
138
- try {
139
- const saved = localStorage.getItem(FLOW_USER_KEY);
140
- return saved ? JSON.parse(saved).email || "you@local" : "you@local";
141
- } catch {
142
- return "you@local";
143
- }
146
+ return readSavedFlowUser()?.email || "you@local";
144
147
  });
145
148
  const [password, setPassword] = React.useState("");
146
- const [name, setName] = React.useState("You");
149
+ const [name, setName] = React.useState(() => readSavedFlowUser()?.name || "You");
147
150
  const [busy, setBusy] = React.useState(false);
148
151
  const [error, setError] = React.useState<string | null>(null);
149
152
 
150
153
  async function submit(event: React.FormEvent) {
151
154
  event.preventDefault();
155
+ const cleanEmail = email.trim();
156
+ const cleanPassword = password.trim();
157
+ const cleanName = name.trim() || cleanEmail.split("@")[0] || "You";
158
+ if (!cleanEmail || !cleanPassword) {
159
+ setError("이름과 이메일, 비밀번호를 입력하면 기존 Brain을 안전하게 확인합니다.");
160
+ return;
161
+ }
152
162
  setBusy(true);
153
163
  setError(null);
154
- const safePassword = password || "Lattice123";
155
- let result = await latticeApi.login(email, safePassword);
156
- if (!result.ok) {
157
- const registered = await latticeApi.register({
158
- email,
159
- password: safePassword,
160
- name: name || email.split("@")[0] || "You",
161
- nickname: name || "You",
162
- });
163
- if (registered.ok) result = await latticeApi.login(email, safePassword);
164
- }
164
+ const savedUser = readSavedFlowUser();
165
+ let result = await latticeApi.login(cleanEmail, cleanPassword);
165
166
  if (!result.ok) {
166
167
  const profile = await latticeApi.profile();
167
- if (!profile.ok) {
168
+ if (profile.ok && (!savedUser?.email || savedUser.email === cleanEmail)) {
169
+ try { localStorage.setItem(FLOW_USER_KEY, JSON.stringify({ email: cleanEmail, name: cleanName })); } catch {}
170
+ setBusy(false);
171
+ onSuccess();
172
+ return;
173
+ }
174
+ if (savedUser?.email && savedUser.email !== cleanEmail) {
175
+ setBusy(false);
176
+ setError("이 컴퓨터의 기존 Brain과 다른 이메일입니다. 오타인지 확인해 주세요.");
177
+ return;
178
+ }
179
+ if (savedUser?.email === cleanEmail) {
168
180
  setBusy(false);
169
- setError("We could not open your local profile. Check your password or create the first local account.");
181
+ setError("기존 Brain 이메일은 맞지만 비밀번호가 다릅니다. 비밀번호를 다시 확인해 주세요.");
170
182
  return;
171
183
  }
184
+ const registered = await latticeApi.register({
185
+ email: cleanEmail,
186
+ password: cleanPassword,
187
+ name: cleanName,
188
+ nickname: cleanName,
189
+ });
190
+ if (registered.ok) result = await latticeApi.login(cleanEmail, cleanPassword);
191
+ }
192
+ if (!result.ok) {
193
+ setBusy(false);
194
+ setError("로컬 프로필을 열 수 없습니다. 이메일과 비밀번호를 확인해 주세요.");
195
+ return;
172
196
  }
173
- try { localStorage.setItem(FLOW_USER_KEY, JSON.stringify({ email, name })); } catch {}
197
+ try { localStorage.setItem(FLOW_USER_KEY, JSON.stringify({ email: cleanEmail, name: cleanName })); } catch {}
174
198
  setBusy(false);
175
199
  onSuccess();
176
200
  }
177
201
 
178
202
  return (
179
203
  <div>
180
- <div className="ritual-title">Welcome to your mind.</div>
204
+ <div className="ritual-title">내 Brain을 시작합니다.</div>
181
205
  <div className="ritual-subtitle">
182
- Models will change. Your knowledge should not. Lattice keeps your documents, conversations, decisions, and context together as a private Brain you own.
206
+ 모델은 바뀔 있지만, 문서와 대화, 결정, 기억은 사라지면 됩니다. Lattice는 지식을 내가 소유하는 개인 Brain으로 모읍니다.
183
207
  </div>
184
208
 
185
209
  <ProductPromise />
@@ -187,26 +211,26 @@ function LoginScreen({ onSuccess }: { onSuccess: () => void }) {
187
211
  <form onSubmit={submit} className="ritual-card" style={{ maxWidth: 420, margin: "0 auto" }}>
188
212
  <div style={{ display: "grid", gap: "0.85rem" }}>
189
213
  <div>
190
- <div style={{ fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "1px", color: "hsl(var(--fg-muted))", marginBottom: 4 }}>Your name</div>
214
+ <div style={{ fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "1px", color: "hsl(var(--fg-muted))", marginBottom: 4 }}>이름</div>
191
215
  <Input value={name} onChange={(e) => setName(e.target.value)} placeholder="You" />
192
216
  </div>
193
217
  <div>
194
- <div style={{ fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "1px", color: "hsl(var(--fg-muted))", marginBottom: 4 }}>Email (local only)</div>
218
+ <div style={{ fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "1px", color: "hsl(var(--fg-muted))", marginBottom: 4 }}>이메일</div>
195
219
  <Input value={email} onChange={(e) => setEmail(e.target.value)} type="email" placeholder="you@local" />
196
220
  </div>
197
221
  <div>
198
- <div style={{ fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "1px", color: "hsl(var(--fg-muted))", marginBottom: 4 }}>Password</div>
199
- <Input value={password} onChange={(e) => setPassword(e.target.value)} type="password" placeholder="Create a strong local password" />
222
+ <div style={{ fontSize: "0.75rem", textTransform: "uppercase", letterSpacing: "1px", color: "hsl(var(--fg-muted))", marginBottom: 4 }}>비밀번호</div>
223
+ <Input value={password} onChange={(e) => setPassword(e.target.value)} type="password" placeholder="로컬 Brain 비밀번호" />
200
224
  </div>
201
225
  </div>
202
226
 
203
227
  {error && <div style={{ marginTop: "0.85rem", padding: "0.6rem 0.85rem", background: "hsl(var(--destructive)/0.12)", border: "1px solid hsl(var(--destructive)/0.4)", borderRadius: 10, fontSize: "0.9rem" }}>{error}</div>}
204
228
 
205
- <Button type="submit" disabled={busy || !email.trim()} style={{ width: "100%", marginTop: "1rem" }}>
206
- {busy ? "Opening the Brain..." : "Open my Brain"}
229
+ <Button type="submit" disabled={busy || !email.trim() || !password.trim()} style={{ width: "100%", marginTop: "1rem" }}>
230
+ {busy ? "Brain 여는 중..." : " Brain 시작하기"}
207
231
  </Button>
208
232
  <div style={{ fontSize: "0.75rem", color: "hsl(var(--fg-muted))", marginTop: "0.6rem" }}>
209
- Local profile first. Model choice comes later.
233
+ 기존 Brain과 다른 이메일이면 새로 만들지 않고 먼저 확인합니다.
210
234
  </div>
211
235
  </form>
212
236
  </div>
@@ -217,16 +241,16 @@ function ProductPromise() {
217
241
  return (
218
242
  <div className="ritual-promise" aria-label="Lattice AI product promise">
219
243
  <div>
220
- <span>Durable knowledge</span>
221
- <strong>Your work becomes long-lived context.</strong>
244
+ <span>오래 남는 지식</span>
245
+ <strong>내 일이 장기 기억이 됩니다.</strong>
222
246
  </div>
223
247
  <div>
224
- <span>Replaceable models</span>
225
- <strong>The model is a voice, not the asset.</strong>
248
+ <span>교체 가능한 모델</span>
249
+ <strong>모델은 목소리이고, 자산은 Brain입니다.</strong>
226
250
  </div>
227
251
  <div>
228
- <span>User ownership</span>
229
- <strong>Back up, restore, and move your Brain.</strong>
252
+ <span>사용자 소유</span>
253
+ <strong>백업, 복원, 이동이 가능합니다.</strong>
230
254
  </div>
231
255
  </div>
232
256
  );
@@ -244,9 +268,9 @@ function AnalysisScreen({
244
268
  const detected = buildDetectedFacts(analysis);
245
269
  return (
246
270
  <div>
247
- <div className="ritual-title">Understanding your home.</div>
271
+ <div className="ritual-title">이 컴퓨터를 확인합니다.</div>
248
272
  <div className="ritual-subtitle">
249
- Lattice is checking what this computer can support so your Brain can run locally instead of turning your memories into a cloud dependency.
273
+ Brain 클라우드에 의존하지 않고 컴퓨터에서 편하게 돌아갈 있는지 확인합니다.
250
274
  </div>
251
275
 
252
276
  <div className="ritual-fact-grid">
@@ -263,9 +287,9 @@ function AnalysisScreen({
263
287
  <div style={{ display: "flex", alignItems: "center", gap: "0.6rem" }}>
264
288
  <Sparkles style={{ color: "hsl(var(--brain-core))" }} />
265
289
  <div>
266
- <div style={{ fontWeight: 620 }}>{analysis ? recommendedSummary(analysis) : "Preparing the best fit..."}</div>
290
+ <div style={{ fontWeight: 620 }}>{analysis ? recommendedSummary(analysis) : "가장 편한 설정을 찾는 중..."}</div>
267
291
  <div style={{ fontSize: "0.9rem", color: "hsl(var(--fg-muted))" }}>
268
- {analysis ? "A short, personal list of minds is ready for you to choose from." : "Reading your machine. This is gentle."}
292
+ {analysis ? "추천 모델을 바로 시작할 있게 준비했습니다." : "잠시만 기다리면 자동으로 정리됩니다."}
269
293
  </div>
270
294
  </div>
271
295
  </div>
@@ -275,7 +299,7 @@ function AnalysisScreen({
275
299
 
276
300
  <div style={{ marginTop: "1.25rem" }}>
277
301
  <Button onClick={onContinue} disabled={!analysis && !error} style={{ minWidth: 260 }}>
278
- See how your Brain can think
302
+ 추천 모델 보기
279
303
  </Button>
280
304
  </div>
281
305
  </div>
@@ -294,12 +318,17 @@ function RecommendationScreen({
294
318
  const items = recommendations.length ? recommendations : [fallbackModel()];
295
319
  return (
296
320
  <div>
297
- <div className="ritual-title">How shall your mind think today?</div>
321
+ <div className="ritual-title">추천 모델로 시작하세요.</div>
298
322
  <div className="ritual-subtitle">
299
- The model is the current voice of your Brain. You can replace it later; your knowledge stays.
323
+ 모델은 Brain 현재 목소리입니다. 나중에 바꿔도 기억과 지식은 그대로 남습니다.
300
324
  </div>
301
325
 
302
326
  <div style={{ maxWidth: 560, margin: "0 auto" }}>
327
+ {items[0]?.supported ? (
328
+ <Button onClick={() => onSelect(items[0])} style={{ width: "100%", marginBottom: "0.85rem" }}>
329
+ 추천대로 시작하기
330
+ </Button>
331
+ ) : null}
303
332
  {items.slice(0, 3).map((model, index) => (
304
333
  <button
305
334
  key={`${model.role}-${model.id}`}
@@ -311,14 +340,14 @@ function RecommendationScreen({
311
340
  <div className="role">{rankLabel(model.role, index)}</div>
312
341
  <div className="name">{model.shortName}</div>
313
342
  <div className="reason">{model.reason} · {model.size || "ready"}</div>
314
- {!model.supported && <div style={{ color: "hsl(var(--destructive))", marginTop: 6, fontSize: "0.85rem" }}>Needs attention on this machine</div>}
343
+ {!model.supported && <div style={{ color: "hsl(var(--destructive))", marginTop: 6, fontSize: "0.85rem" }}>이 컴퓨터에서 추가 확인이 필요합니다</div>}
315
344
  </button>
316
345
  ))}
317
346
  </div>
318
347
 
319
348
  <div style={{ marginTop: "1.1rem", display: "flex", justifyContent: "center", gap: "1rem", alignItems: "center" }}>
320
- <Button variant="ghost" onClick={onBack}>Back</Button>
321
- <div style={{ fontSize: "0.82rem", color: "hsl(var(--fg-muted))" }}>Your choice becomes the current voice of your Brain.</div>
349
+ <Button variant="ghost" onClick={onBack}>뒤로</Button>
350
+ <div style={{ fontSize: "0.82rem", color: "hsl(var(--fg-muted))" }}>잘 모르겠다면 추천대로 시작하면 됩니다.</div>
322
351
  </div>
323
352
  </div>
324
353
  );
@@ -336,7 +365,7 @@ function InstallScreen({
336
365
  const [busy, setBusy] = React.useState(false);
337
366
  const [stage, setStage] = React.useState<InstallStage>("idle");
338
367
  const [percent, setPercent] = React.useState(0);
339
- const [message, setMessage] = React.useState("Your Brain is waiting for this mind.");
368
+ const [message, setMessage] = React.useState("Brain 사용할 모델을 기다리고 있습니다.");
340
369
  const [error, setError] = React.useState<string | null>(null);
341
370
 
342
371
  async function start() {
@@ -344,7 +373,7 @@ function InstallScreen({
344
373
  setError(null);
345
374
  setStage("install");
346
375
  setPercent(8);
347
- setMessage("Preparing the Brain.");
376
+ setMessage("Brain 준비 중입니다.");
348
377
  const result = await latticeApi.streamModelPrepare(
349
378
  { model: model.loadId, engine: model.engine || "local_mlx", allow_download: true },
350
379
  {
@@ -357,7 +386,7 @@ function InstallScreen({
357
386
  onDone: () => {
358
387
  setStage("done");
359
388
  setPercent(100);
360
- setMessage("Your Brain is ready.");
389
+ setMessage("Brain 준비되었습니다.");
361
390
  },
362
391
  onError: (event) => {
363
392
  setStage("error");
@@ -369,7 +398,7 @@ function InstallScreen({
369
398
  if (result.ok) {
370
399
  setStage("done");
371
400
  setPercent(100);
372
- setMessage("Your Brain is ready.");
401
+ setMessage("Brain 준비되었습니다.");
373
402
  window.setTimeout(onComplete, 700);
374
403
  } else {
375
404
  setStage("error");
@@ -385,10 +414,10 @@ function InstallScreen({
385
414
 
386
415
  return (
387
416
  <div>
388
- <div className="ritual-title">Bring this mind home.</div>
417
+ <div className="ritual-title">모델을 설치하고 시작합니다.</div>
389
418
  <div className="ritual-subtitle">
390
419
  <strong>{model.shortName}</strong> — {model.reason}.<br />
391
- This gives your Brain a local voice. Download, validation, and loading happen only with your consent.
420
+ 모델이 Brain 로컬 목소리가 됩니다. 다운로드, 확인, 로드는 사용자가 누른 뒤에만 진행됩니다.
392
421
  </div>
393
422
 
394
423
  {/* Living Brain reacts to the ceremony of installation */}
@@ -416,31 +445,34 @@ function InstallScreen({
416
445
  </div>
417
446
 
418
447
  <div className="ritual-status">{message}</div>
448
+ <div className="ritual-card" style={{ margin: "0.8rem auto 0", maxWidth: 540, fontSize: "0.86rem", color: "hsl(var(--fg-muted))" }}>
449
+ 큰 모델은 다운로드에 몇 분 이상 걸릴 수 있습니다. 진행률은 모델 런타임이 알려주는 만큼 표시하고, 멈춘 것처럼 보여도 현재 단계가 유지됩니다.
450
+ </div>
419
451
 
420
452
  {error && (
421
453
  <div className="ritual-card" style={{ borderColor: "hsl(var(--destructive)/0.45)", background: "hsl(var(--destructive)/0.07)", marginBottom: "1rem" }}>
422
454
  {error}
423
- <div style={{ marginTop: "0.5rem", fontSize: "0.85rem" }}>You can go back and choose a different mind, or try again.</div>
455
+ <div style={{ marginTop: "0.5rem", fontSize: "0.85rem" }}>다른 모델을 고르거나 다시 시도할 있습니다.</div>
424
456
  </div>
425
457
  )}
426
458
 
427
459
  <div style={{ display: "flex", gap: "0.75rem", justifyContent: "center", marginTop: "1rem" }}>
428
- <Button variant="ghost" onClick={onBack} disabled={busy}>Choose differently</Button>
460
+ <Button variant="ghost" onClick={onBack} disabled={busy}>다른 모델 고르기</Button>
429
461
 
430
462
  {stage !== "done" ? (
431
463
  <Button
432
464
  onClick={start}
433
465
  disabled={busy || !model.supported}
434
466
  >
435
- {busy ? "Waking the mind..." : "Yes — make this my Brain"}
467
+ {busy ? "모델 준비 중..." : "다운로드하고 시작하기"}
436
468
  </Button>
437
469
  ) : (
438
- <Button onClick={onComplete}>Enter your Brain</Button>
470
+ <Button onClick={onComplete}>Brain으로 들어가기</Button>
439
471
  )}
440
472
  </div>
441
473
 
442
474
  <div style={{ fontSize: "0.72rem", color: "hsl(var(--fg-muted))", marginTop: "0.9rem" }}>
443
- Explicit consent only. All work happens locally on your machine.
475
+ 모든 작업은 컴퓨터에서 진행됩니다.
444
476
  </div>
445
477
  </div>
446
478
  );
@@ -590,7 +590,7 @@ function AdminPanel() {
590
590
  const mode = useAppStore((state) => state.mode);
591
591
  const summary = useQuery({ queryKey: ["adminSummary"], queryFn: latticeApi.adminSummary });
592
592
  const users = useQuery({ queryKey: ["adminUsers"], queryFn: latticeApi.adminUsers });
593
- const audit = useQuery({ queryKey: ["adminAudit"], queryFn: latticeApi.adminAudit });
593
+ const audit = useQuery({ queryKey: ["adminAudit"], queryFn: () => latticeApi.adminAudit() });
594
594
  const roles = useQuery({ queryKey: ["adminRoles"], queryFn: latticeApi.adminRoles });
595
595
  const policies = useQuery({ queryKey: ["adminPolicies"], queryFn: latticeApi.adminPolicies });
596
596
  const hardening = useQuery({ queryKey: ["adminProductHardening"], queryFn: latticeApi.adminProductHardening });
@@ -1298,6 +1298,107 @@ body {
1298
1298
  mask-image: linear-gradient(transparent, black 6%, black 92%, transparent);
1299
1299
  }
1300
1300
 
1301
+ .brain-overview-panel {
1302
+ margin: 0 0 1rem;
1303
+ border: 1px solid hsl(var(--border) / 0.58);
1304
+ border-radius: 8px;
1305
+ background: hsl(var(--surface-glass));
1306
+ padding: 0.8rem;
1307
+ box-shadow: 0 14px 34px hsl(200 30% 3% / 0.16);
1308
+ backdrop-filter: blur(18px);
1309
+ }
1310
+
1311
+ .brain-overview-head {
1312
+ display: flex;
1313
+ align-items: center;
1314
+ justify-content: space-between;
1315
+ gap: 0.8rem;
1316
+ margin-bottom: 0.72rem;
1317
+ }
1318
+
1319
+ .brain-overview-head span {
1320
+ display: block;
1321
+ color: hsl(var(--fg-muted));
1322
+ font-size: 0.68rem;
1323
+ font-weight: 820;
1324
+ }
1325
+
1326
+ .brain-overview-head strong {
1327
+ display: block;
1328
+ margin-top: 0.14rem;
1329
+ font-size: 0.94rem;
1330
+ }
1331
+
1332
+ .brain-overview-head button {
1333
+ min-height: 2rem;
1334
+ border: 1px solid hsl(var(--brain-core) / 0.38);
1335
+ border-radius: 999px;
1336
+ background: hsl(var(--brain-core) / 0.08);
1337
+ color: hsl(var(--fg));
1338
+ padding: 0 0.75rem;
1339
+ font-size: 0.78rem;
1340
+ font-weight: 760;
1341
+ }
1342
+
1343
+ .brain-overview-grid {
1344
+ display: grid;
1345
+ grid-template-columns: repeat(3, minmax(0, 1fr));
1346
+ gap: 0.62rem;
1347
+ }
1348
+
1349
+ .brain-overview-column {
1350
+ display: grid;
1351
+ min-height: 6.4rem;
1352
+ align-content: start;
1353
+ gap: 0.34rem;
1354
+ border: 1px solid hsl(var(--border) / 0.5);
1355
+ border-radius: 8px;
1356
+ background: hsl(var(--bg) / 0.38);
1357
+ color: hsl(var(--fg));
1358
+ padding: 0.66rem;
1359
+ text-align: left;
1360
+ transition: border-color 150ms ease, background 150ms ease;
1361
+ }
1362
+
1363
+ .brain-overview-column:hover,
1364
+ .brain-overview-column:focus-visible {
1365
+ border-color: hsl(var(--brain-core) / 0.65);
1366
+ background: hsl(var(--brain-core) / 0.07);
1367
+ outline: none;
1368
+ }
1369
+
1370
+ .brain-overview-column span {
1371
+ color: hsl(var(--fg-muted));
1372
+ font-size: 0.68rem;
1373
+ font-weight: 820;
1374
+ }
1375
+
1376
+ .brain-overview-column strong {
1377
+ display: block;
1378
+ overflow: hidden;
1379
+ text-overflow: ellipsis;
1380
+ white-space: nowrap;
1381
+ font-size: 0.8rem;
1382
+ line-height: 1.2;
1383
+ }
1384
+
1385
+ .brain-overview-column em {
1386
+ color: hsl(var(--fg-muted));
1387
+ font-size: 0.8rem;
1388
+ font-style: normal;
1389
+ line-height: 1.35;
1390
+ }
1391
+
1392
+ .brain-save-feedback {
1393
+ border: 1px solid hsl(var(--memory) / 0.38);
1394
+ border-radius: 8px;
1395
+ background: hsl(var(--memory) / 0.08);
1396
+ color: hsl(var(--fg));
1397
+ padding: 0.58rem 0.72rem;
1398
+ font-size: 0.84rem;
1399
+ font-weight: 720;
1400
+ }
1401
+
1301
1402
  .brain-message {
1302
1403
  max-width: 78%;
1303
1404
  margin-bottom: 1.1rem;
@@ -1741,6 +1842,43 @@ body {
1741
1842
  line-height: 1.05;
1742
1843
  }
1743
1844
 
1845
+ .brain-depth-actions {
1846
+ position: absolute;
1847
+ left: 50%;
1848
+ bottom: 4.15rem;
1849
+ z-index: 6;
1850
+ display: flex;
1851
+ max-width: min(94vw, 31rem);
1852
+ transform: translateX(-50%);
1853
+ gap: 0.35rem;
1854
+ border: 1px solid hsl(var(--border) / 0.52);
1855
+ border-radius: 999px;
1856
+ background: hsl(var(--surface-glass));
1857
+ padding: 0.32rem;
1858
+ box-shadow: 0 14px 38px hsl(200 30% 3% / 0.22);
1859
+ backdrop-filter: blur(18px);
1860
+ }
1861
+
1862
+ .brain-depth-actions button {
1863
+ min-height: 2rem;
1864
+ border: 0;
1865
+ border-radius: 999px;
1866
+ background: transparent;
1867
+ color: hsl(var(--fg-muted));
1868
+ padding: 0 0.68rem;
1869
+ font-size: 0.78rem;
1870
+ font-weight: 760;
1871
+ white-space: nowrap;
1872
+ }
1873
+
1874
+ .brain-depth-actions button:hover,
1875
+ .brain-depth-actions button:focus-visible,
1876
+ .brain-depth-actions button.is-active {
1877
+ background: hsl(var(--brain-core) / 0.12);
1878
+ color: hsl(var(--fg));
1879
+ outline: none;
1880
+ }
1881
+
1744
1882
  .memory-fragment,
1745
1883
  .concept-signal,
1746
1884
  .graph-node {
@@ -2349,6 +2487,43 @@ body {
2349
2487
  gap: 0.45rem;
2350
2488
  }
2351
2489
 
2490
+ .admin-log-filters {
2491
+ display: grid;
2492
+ grid-template-columns: minmax(0, 1fr) minmax(8.5rem, 0.65fr) auto;
2493
+ gap: 0.45rem;
2494
+ align-items: center;
2495
+ }
2496
+
2497
+ .admin-log-filters label {
2498
+ display: flex;
2499
+ min-width: 0;
2500
+ align-items: center;
2501
+ gap: 0.4rem;
2502
+ border: 1px solid hsl(var(--border) / 0.54);
2503
+ border-radius: 8px;
2504
+ background: hsl(var(--bg) / 0.34);
2505
+ padding: 0.44rem 0.52rem;
2506
+ color: hsl(var(--fg-muted));
2507
+ }
2508
+
2509
+ .admin-log-filters input,
2510
+ .admin-log-filters select {
2511
+ min-width: 0;
2512
+ width: 100%;
2513
+ border: 0;
2514
+ outline: 0;
2515
+ background: transparent;
2516
+ color: hsl(var(--fg));
2517
+ font: inherit;
2518
+ font-size: 0.78rem;
2519
+ }
2520
+
2521
+ .admin-log-filters span,
2522
+ .admin-retention span {
2523
+ color: hsl(var(--fg-muted));
2524
+ font-size: 0.76rem;
2525
+ }
2526
+
2352
2527
  .admin-list-row {
2353
2528
  display: flex;
2354
2529
  min-height: 3.15rem;
@@ -2427,6 +2602,20 @@ body {
2427
2602
  font-size: 0.74rem;
2428
2603
  }
2429
2604
 
2605
+ .admin-retention {
2606
+ display: grid;
2607
+ gap: 0.18rem;
2608
+ border: 1px solid hsl(var(--border) / 0.48);
2609
+ border-radius: 8px;
2610
+ background: hsl(var(--bg) / 0.35);
2611
+ padding: 0.68rem;
2612
+ }
2613
+
2614
+ .admin-retention strong {
2615
+ color: hsl(var(--fg));
2616
+ font-size: 0.88rem;
2617
+ }
2618
+
2430
2619
  .brain-image-input {
2431
2620
  display: inline-flex;
2432
2621
  cursor: pointer;
@@ -2583,6 +2772,22 @@ body {
2583
2772
  .brain-organism { width: 200px; height: 200px; }
2584
2773
  .brain-conversation { padding-bottom: 5.5rem; }
2585
2774
  .brain-conversation-header { flex-wrap: wrap; }
2775
+ .brain-depth-actions {
2776
+ bottom: 3.55rem;
2777
+ width: min(100vw - 1rem, 24rem);
2778
+ overflow-x: auto;
2779
+ justify-content: flex-start;
2780
+ }
2781
+ .brain-depth-actions button {
2782
+ flex: 0 0 auto;
2783
+ }
2784
+ .brain-overview-grid {
2785
+ grid-template-columns: 1fr;
2786
+ }
2787
+ .brain-overview-head {
2788
+ align-items: flex-start;
2789
+ flex-direction: column;
2790
+ }
2586
2791
  .brain-care-summary {
2587
2792
  display: grid;
2588
2793
  }
@@ -26,7 +26,7 @@ from .storage import (
26
26
  storage_from_env,
27
27
  )
28
28
 
29
- __version__ = "4.7.0"
29
+ __version__ = "4.7.2"
30
30
 
31
31
  __all__ = [
32
32
  "AgentRuntime",
@@ -14,7 +14,7 @@ from datetime import datetime
14
14
  from typing import Any, Callable, Dict, List, Optional
15
15
 
16
16
 
17
- MULTI_AGENT_VERSION = "4.7.0"
17
+ MULTI_AGENT_VERSION = "4.7.2"
18
18
 
19
19
  AGENT_ROLES = ("researcher", "planner", "executor", "reviewer", "release")
20
20
  CORE_PIPELINE = ("planner", "executor", "reviewer")
@@ -1,3 +1,3 @@
1
1
  """Lattice AI - modular server package."""
2
2
 
3
- __version__ = "4.7.0"
3
+ __version__ = "4.7.2"