ltcai 6.3.0 → 6.3.1

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.md CHANGED
@@ -69,42 +69,42 @@ You need Lattice AI when:
69
69
  Choose the owner of the Brain. The profile is not a SaaS account by default; it
70
70
  is the local identity for the knowledge you keep.
71
71
 
72
- ![Login](output/release/v6.3.0/screenshots/01-login.png)
72
+ ![Login](output/release/v6.3.1/screenshots/01-login.png)
73
73
 
74
74
  ### 2. Environment Analysis
75
75
 
76
76
  See what kind of local AI experience this computer can support before choosing a
77
77
  model.
78
78
 
79
- ![Environment Analysis](output/release/v6.3.0/screenshots/02-environment-analysis.png)
79
+ ![Environment Analysis](output/release/v6.3.1/screenshots/02-environment-analysis.png)
80
80
 
81
81
  ### 3. Recommended Models
82
82
 
83
83
  Start with a short list: safest recommendation, faster model, stronger model.
84
84
  Advanced details stay available without overwhelming first-time users.
85
85
 
86
- ![Recommended Models](output/release/v6.3.0/screenshots/03-recommended-models.png)
86
+ ![Recommended Models](output/release/v6.3.1/screenshots/03-recommended-models.png)
87
87
 
88
88
  ### 4. Install And Load
89
89
 
90
90
  Download and load only after consent. Lattice explains model size, local
91
91
  execution, and network use before work starts.
92
92
 
93
- ![Install and Load](output/release/v6.3.0/screenshots/04-install-load-progress.png)
93
+ ![Install and Load](output/release/v6.3.1/screenshots/04-install-load-progress.png)
94
94
 
95
95
  ### 5. Brain Chat
96
96
 
97
97
  Talk normally. Useful decisions and context become memory, then appear later as
98
98
  topics, relationships, and graph structure.
99
99
 
100
- ![Brain Chat Home](output/release/v6.3.0/screenshots/05-brain-chat-home.png)
100
+ ![Brain Chat Home](output/release/v6.3.1/screenshots/05-brain-chat-home.png)
101
101
 
102
102
  ### 6. Review Center
103
103
 
104
104
  Automation results are staged for review before they become durable decisions.
105
105
  Snooze, unsnooze, run now, approve, and dismiss actions stay explicit.
106
106
 
107
- ![Review Center](output/release/v6.3.0/screenshots/13-review-center.png)
107
+ ![Review Center](output/release/v6.3.1/screenshots/13-review-center.png)
108
108
 
109
109
  ## Brain Depths
110
110
 
@@ -120,10 +120,10 @@ The user travels inward from everyday memory to deeper structure:
120
120
 
121
121
  Walkthrough:
122
122
 
123
- ![v6.3.0 Living Brain walkthrough](output/release/v6.3.0/gifs/v6.3.0-living-brain-walkthrough.gif)
123
+ ![v6.3.1 Living Brain walkthrough](output/release/v6.3.1/gifs/v6.3.1-living-brain-walkthrough.gif)
124
124
 
125
125
  Screenshot index and capture notes:
126
- [output/release/v6.3.0/SCREENSHOT_INDEX.md](output/release/v6.3.0/SCREENSHOT_INDEX.md)
126
+ [output/release/v6.3.1/SCREENSHOT_INDEX.md](output/release/v6.3.1/SCREENSHOT_INDEX.md)
127
127
 
128
128
  ## Install
129
129
 
@@ -200,7 +200,7 @@ See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for developer workflow details.
200
200
 
201
201
  ## Current Release Preparation
202
202
 
203
- The current development target is **6.3.0 Product Hardening Completion**:
203
+ The current development target is **6.3.1 Access Runtime / i18n Follow-up**:
204
204
 
205
205
  - `App.tsx` now delegates the main Brain and Admin surfaces to focused feature
206
206
  modules instead of owning the full product shell.
@@ -220,14 +220,19 @@ The current development target is **6.3.0 Product Hardening Completion**:
220
220
  - Brain archive, memory provenance, document ingestion, Review Center Run Now,
221
221
  local model runtime, root-shim smoke, and app-factory review wiring now have
222
222
  dedicated UX/backend hardening coverage for the 6.3 line.
223
+ - Access-control helper closures are extracted from `app_factory.py` into a
224
+ focused runtime module with direct unit coverage for role lookup, token
225
+ extraction, user enforcement, admin enforcement, and public user projection.
226
+ - Capture and Review Center copy now route through the shared Korean/English
227
+ i18n map for tabs, placeholders, action labels, empty states, and feedback.
223
228
 
224
- Expected artifacts for 6.3.0 release must use exact filenames:
229
+ Expected artifacts for 6.3.1 release must use exact filenames:
225
230
 
226
- - `dist/ltcai-6.3.0-py3-none-any.whl`
227
- - `dist/ltcai-6.3.0.tar.gz`
228
- - `ltcai-6.3.0.tgz`
229
- - `dist/ltcai-6.3.0.vsix`
230
- - `src-tauri/target/release/bundle/dmg/Lattice AI_6.3.0_aarch64.dmg`
231
+ - `dist/ltcai-6.3.1-py3-none-any.whl`
232
+ - `dist/ltcai-6.3.1.tar.gz`
233
+ - `ltcai-6.3.1.tgz`
234
+ - `dist/ltcai-6.3.1.vsix`
235
+ - `src-tauri/target/release/bundle/dmg/Lattice AI_6.3.1_aarch64.dmg`
231
236
 
232
237
  Do not upload `dist/*`. Package registry publishing remains owner-run.
233
238
 
@@ -246,6 +251,10 @@ Do not upload `dist/*`. Package registry publishing remains owner-run.
246
251
 
247
252
  | Version | Theme |
248
253
  | --- | --- |
254
+ | 6.3.1 | Access Runtime / i18n Follow-up: app-factory access-control extraction, focused access runtime tests, and Capture/Review Center i18n coverage |
255
+ | 6.3.0 | Product Hardening Completion: Brain archive/provenance/ingestion UX polish, Review Center Run Now contract hardening, local model runtime status, app-factory review wiring, shim smoke, i18n guard, and exact release artifacts |
256
+ | 6.2.0 | Product Decomposition / Release Smoke Automation: App and ProductFlow feature extraction, legacy root shim shrink, model download consent UX, typed router contexts, localized smoke tests, and wheel/npm/static/Tauri release smoke |
257
+ | 6.1.0 | Product Hardening / Digital Brain Completion: local Brain entry without a loaded model, Brain memory/backup loop, Review Center semantics, ToolRegistry authorization boundary, and root CLI/runtime compatibility |
249
258
  | 6.0.0 | Product Reset / Review Center Completion: Snoozed filter, Unsnooze, OpenAPI-derived Review typing, Review feature extraction, v6 docs and scorecard |
250
259
  | 5.6.0 | Brain Automation Review Center: workspace-scoped automation review inbox, source-aware provenance, guarded approve/dismiss/snooze/run_now actions, and Act Review tab |
251
260
  | 5.5.0 | Release Coordination: synchronized package/runtime/static versions and release docs for the 5.5.0 line while preserving v5.4.0 Brain Automation Scheduler behavior |
package/docs/CHANGELOG.md CHANGED
@@ -3,6 +3,30 @@
3
3
  The top entry is the current release-preparation target. Older entries are
4
4
  historical and may describe behavior as it existed at that release.
5
5
 
6
+ ## [6.3.1] - 2026-06-17
7
+
8
+ > Access Runtime / i18n Follow-up. Closes the next app-factory decomposition
9
+ > slice and finishes Capture/Review Center localization coverage on top of the
10
+ > v6.3.0 product-hardening line.
11
+
12
+ ### Changed
13
+ - Moved app-factory access-control helper closures into
14
+ `latticeai.runtime.access_runtime` while preserving role lookup, token
15
+ extraction, user enforcement, admin enforcement, and public user projection
16
+ call signatures.
17
+ - Capture tabs, headings, placeholders, action labels, upload details, folder
18
+ connection copy, browser capture copy, and pipeline controls now use the
19
+ shared Korean/English i18n map.
20
+ - Review Center inbox/card copy, status badges, empty states, Run Now feedback,
21
+ snooze/unsnooze labels, and action aria labels now use the shared i18n map.
22
+ - README release evidence screenshots and walkthrough GIF are refreshed under
23
+ `output/release/v6.3.1/`.
24
+ - Package/runtime/static metadata is synchronized to 6.3.1; package publish and
25
+ deployment remain owner-run only.
26
+
27
+ ### Added
28
+ - Focused unit coverage for the extracted access runtime auth/admin behavior.
29
+
6
30
  ## [6.3.0] - 2026-06-16
7
31
 
8
32
  > Product Hardening Completion. Completes the 6.2 decomposition follow-up by
@@ -3,6 +3,7 @@ import type { ApiResult, ReviewItem } from "@/api/client";
3
3
  import { ActionButton, KeyValueList } from "@/components/primitives";
4
4
  import { Badge } from "@/components/ui/badge";
5
5
  import { Button } from "@/components/ui/button";
6
+ import { t } from "@/i18n";
6
7
  import { useAppStore } from "@/store/appStore";
7
8
  import {
8
9
  formatSnoozedUntil,
@@ -22,6 +23,7 @@ type ReviewCardProps = {
22
23
 
23
24
  export function ReviewCard({ item, feedback, onAction }: ReviewCardProps) {
24
25
  const mode = useAppStore((state) => state.mode);
26
+ const language = useAppStore((state) => state.language);
25
27
  const provenance = item.provenance || {};
26
28
  const payload = item.payload || {};
27
29
  const hadRun = hasRunBefore(item);
@@ -45,10 +47,10 @@ export function ReviewCard({ item, feedback, onAction }: ReviewCardProps) {
45
47
  <div className="mt-3 flex flex-wrap items-center justify-between gap-3 rounded-md border border-border bg-muted/24 p-3 text-sm">
46
48
  <div>
47
49
  <div className="font-medium">{formatSnoozedUntil(item.snoozed_until)}</div>
48
- <p className="mt-1 text-muted-foreground">This stays out of the pending queue until then. Unsnooze brings it back immediately.</p>
50
+ <p className="mt-1 text-muted-foreground">{t(language, "review.snoozed.detail")}</p>
49
51
  </div>
50
52
  <Button size="sm" variant="outline" onClick={() => onAction(item, "unsnooze")} disabled={!actionable}>
51
- <RotateCcw className="h-3.5 w-3.5" /> Unsnooze
53
+ <RotateCcw className="h-3.5 w-3.5" /> {t(language, "review.unsnooze")}
52
54
  </Button>
53
55
  </div>
54
56
  ) : null}
@@ -73,24 +75,24 @@ export function ReviewCard({ item, feedback, onAction }: ReviewCardProps) {
73
75
  {actionable ? (
74
76
  <div className="mt-4 grid gap-2">
75
77
  <p className="text-xs leading-5 text-muted-foreground">
76
- Run now previews the action without approving it. Approve or dismiss when the result looks right.
78
+ {t(language, "review.runNow.detail")}
77
79
  </p>
78
- <div className="flex flex-wrap gap-2" aria-label="Review actions">
80
+ <div className="flex flex-wrap gap-2" aria-label={t(language, "review.actions.aria")}>
79
81
  <ActionButton
80
- label="Run now"
81
- successLabel={hadRun ? "Regenerated" : "Executed"}
82
+ label={t(language, "review.runNow")}
83
+ successLabel={hadRun ? t(language, "review.regenerated") : t(language, "review.executed")}
82
84
  action={() => onAction(item, "run_now", hadRun)}
83
85
  invalidate={[]}
84
86
  />
85
- <ActionButton label="Approve" action={() => onAction(item, "approve")} invalidate={[]} />
86
- {!snoozed ? <ActionButton label="Snooze 1 day" action={() => onAction(item, "snooze")} invalidate={[]} /> : null}
87
- <ActionButton label="Dismiss" action={() => onAction(item, "dismiss")} invalidate={[]} variant="destructive" />
87
+ <ActionButton label={t(language, "review.approve")} action={() => onAction(item, "approve")} invalidate={[]} />
88
+ {!snoozed ? <ActionButton label={t(language, "review.snoozeDay")} action={() => onAction(item, "snooze")} invalidate={[]} /> : null}
89
+ <ActionButton label={t(language, "review.dismiss")} action={() => onAction(item, "dismiss")} invalidate={[]} variant="destructive" />
88
90
  </div>
89
91
  </div>
90
92
  ) : null}
91
93
  {feedback ? (
92
94
  <p className={`mt-2 text-xs ${/fail|error|unavailable/i.test(feedback) ? "text-amber-300" : "text-emerald-300"}`}>
93
- {feedback} - item stays open until you approve or dismiss.
95
+ {feedback} - {t(language, "review.feedback.open")}
94
96
  </p>
95
97
  ) : null}
96
98
  </div>
@@ -4,6 +4,8 @@ import { latticeApi, type ReviewItem, type ReviewSourceFilter, type ReviewStatus
4
4
  import { EmptyState, LoadingPanel, Tabs } from "@/components/primitives";
5
5
  import { Badge } from "@/components/ui/badge";
6
6
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
7
+ import { t } from "@/i18n";
8
+ import { useAppStore } from "@/store/appStore";
7
9
  import { ReviewCard } from "./ReviewCard";
8
10
  import {
9
11
  defaultSnoozeUntil,
@@ -13,6 +15,7 @@ import {
13
15
  } from "./reviewHelpers";
14
16
 
15
17
  export function ReviewInbox() {
18
+ const language = useAppStore((state) => state.language);
16
19
  const qc = useQueryClient();
17
20
  const [statusFilter, setStatusFilter] = React.useState<ReviewStatusFilter>("pending");
18
21
  const [sourceFilter, setSourceFilter] = React.useState<ReviewSourceFilter>("all");
@@ -41,7 +44,7 @@ export function ReviewInbox() {
41
44
  if (!result.ok) {
42
45
  setRunFeedback((prev) => ({
43
46
  ...prev,
44
- [item.id]: result.error || `${action} failed`,
47
+ [item.id]: result.error || t(language, "review.action.failed", { action }),
45
48
  }));
46
49
  return result;
47
50
  }
@@ -52,7 +55,9 @@ export function ReviewInbox() {
52
55
  const runId = String(payload.last_run_id || provenance.run_id || "");
53
56
  setRunFeedback((prev) => ({
54
57
  ...prev,
55
- [item.id]: runId ? `${hadRunBefore ? "Regenerated" : "Executed"} · ${runId}` : hadRunBefore ? "Regenerated" : "Executed",
58
+ [item.id]: runId
59
+ ? `${hadRunBefore ? t(language, "review.regenerated") : t(language, "review.executed")} · ${runId}`
60
+ : hadRunBefore ? t(language, "review.regenerated") : t(language, "review.executed"),
56
61
  }));
57
62
  } else {
58
63
  setRunFeedback((prev) => {
@@ -66,18 +71,18 @@ export function ReviewInbox() {
66
71
  return result;
67
72
  };
68
73
 
69
- if (reviews.isLoading) return <LoadingPanel title="Review inbox" />;
74
+ if (reviews.isLoading) return <LoadingPanel title={t(language, "review.inbox.title")} />;
70
75
 
71
76
  return (
72
77
  <Card>
73
78
  <CardHeader className="gap-3">
74
79
  <div className="flex flex-wrap items-start justify-between gap-3">
75
80
  <div>
76
- <CardTitle>Review inbox</CardTitle>
77
- <CardDescription>Automation suggestions waiting for your decision. Run now executes without approving.</CardDescription>
81
+ <CardTitle>{t(language, "review.inbox.title")}</CardTitle>
82
+ <CardDescription>{t(language, "review.inbox.description")}</CardDescription>
78
83
  </div>
79
84
  {reviews.data ? (
80
- <Badge variant={reviews.data.ok ? "success" : "warning"}>{reviews.data.ok ? "connected" : "unavailable"}</Badge>
85
+ <Badge variant={reviews.data.ok ? "success" : "warning"}>{reviews.data.ok ? t(language, "review.status.connected") : t(language, "review.status.unavailable")}</Badge>
81
86
  ) : null}
82
87
  </div>
83
88
  <div className="grid gap-2">
@@ -96,13 +101,13 @@ export function ReviewInbox() {
96
101
  <CardContent>
97
102
  {reviews.isError || (reviews.data && !reviews.data.ok) ? (
98
103
  <EmptyState
99
- title="Could not load review inbox"
100
- detail={reviews.data?.error || "The review queue is not available right now."}
104
+ title={t(language, "review.inbox.loadError")}
105
+ detail={reviews.data?.error || t(language, "review.inbox.unavailable")}
101
106
  />
102
107
  ) : !items.length ? (
103
108
  <EmptyState
104
- title="Nothing to review"
105
- detail={statusFilter === "snoozed" ? "Snoozed items will appear here until they are unsnoozed or become pending again." : "When automations opt into the review queue, new suggestions will appear here."}
109
+ title={t(language, "review.inbox.empty")}
110
+ detail={statusFilter === "snoozed" ? t(language, "review.inbox.emptySnoozed") : t(language, "review.inbox.emptyPending")}
106
111
  />
107
112
  ) : (
108
113
  <div className="grid gap-3">
@@ -109,6 +109,64 @@ export const COPY: Record<Language, TextMap> = {
109
109
  "care.result.path": "파일: {path}",
110
110
  "care.result.contents": "기억 {memories}개 · 연결 {links}개 · 대화 {conversations}개",
111
111
  "care.result.dryRun": "미리보기만 실행되어 현재 Brain은 바뀌지 않았습니다.",
112
+ "capture.tab.files": "파일",
113
+ "capture.tab.local": "폴더",
114
+ "capture.tab.browser": "웹",
115
+ "capture.tab.pipeline": "흐름",
116
+ "capture.kicker": "추가",
117
+ "capture.title": "중요한 자료를 Brain에 넣으세요.",
118
+ "capture.body": "파일을 넣고, 폴더를 연결하고, 웹페이지를 저장하세요. Lattice는 배운 생각의 출처를 함께 기억합니다.",
119
+ "capture.retry": "다시 시도",
120
+ "capture.files.title": "문서 추가",
121
+ "capture.files.description": "파일을 선택하면 Lattice가 검색과 기억에 들어갈 수 있게 준비합니다.",
122
+ "capture.files.drop": "파일을 놓거나 문서를 선택",
123
+ "capture.files.dropDetail": "각 파일은 대기열에 들어가고, 파싱된 뒤 Memory에 저장되며, 출처 메타데이터와 함께 Brain 그래프에 연결됩니다.",
124
+ "capture.files.uploaded": "업로드된 문서",
125
+ "capture.files.completed": "완료된 업로드는 Memory에 들어간 뒤 여기에 표시됩니다. 그래프 링크는 파싱 후 잠시 더 만들어질 수 있습니다.",
126
+ "capture.files.success": "Brain과 그래프 대기열에 들어감",
127
+ "capture.files.queued": "수집 대기열에서 대기 중",
128
+ "capture.files.uploading": "파싱 후 Memory로 전송 중",
129
+ "capture.files.failed": "Brain에 들어가기 전에 수집에 실패했습니다.",
130
+ "capture.files.captured": "출처 메타데이터와 함께 수집됨",
131
+ "capture.files.capturedWithNode": "출처 메타데이터와 함께 수집됨 · {node}",
132
+ "capture.local.title": "폴더 연결",
133
+ "capture.local.description": "Lattice가 기억해야 할 폴더를 지정하세요.",
134
+ "capture.local.placeholder": "이 Mac의 폴더 경로",
135
+ "capture.local.connect": "폴더 연결",
136
+ "capture.local.success": "폴더 연결 요청됨",
137
+ "capture.local.sources": "연결된 출처",
138
+ "capture.local.access": "폴더 접근",
139
+ "capture.browser.title": "웹페이지 저장",
140
+ "capture.browser.description": "유용한 내용을 기억할 수 있게 페이지를 수집합니다.",
141
+ "capture.browser.placeholder": "https://example.com/article",
142
+ "capture.browser.capture": "URL 수집",
143
+ "capture.browser.success": "URL 수집 요청됨",
144
+ "capture.pipeline.status": "처리 상태",
145
+ "capture.pipeline.growth": "Brain 성장",
146
+ "capture.pipeline.refresh": "기억 새로고침",
147
+ "capture.pipeline.description": "수집한 자료를 Lattice가 다시 확인해야 할 때 검색을 새로고침합니다.",
148
+ "capture.pipeline.rebuild": "검색 인덱스 다시 만들기",
149
+ "review.inbox.title": "검토함",
150
+ "review.inbox.description": "결정 대기 중인 자동화 제안입니다. 지금 실행은 승인 없이 미리보기만 수행합니다.",
151
+ "review.status.connected": "연결됨",
152
+ "review.status.unavailable": "사용 불가",
153
+ "review.inbox.loadError": "검토함을 불러올 수 없습니다.",
154
+ "review.inbox.unavailable": "지금은 검토 대기열을 사용할 수 없습니다.",
155
+ "review.inbox.empty": "검토할 항목 없음",
156
+ "review.inbox.emptySnoozed": "일시 중지된 항목은 해제되거나 다시 대기 상태가 될 때까지 여기에 표시됩니다.",
157
+ "review.inbox.emptyPending": "자동화가 검토 대기열을 사용하면 새 제안이 여기에 표시됩니다.",
158
+ "review.snoozed.detail": "해당 시점까지 대기열에서 빠져 있습니다. 일시 중지 해제 시 즉시 돌아옵니다.",
159
+ "review.unsnooze": "일시 중지 해제",
160
+ "review.runNow.detail": "지금 실행은 승인 없이 작업을 미리봅니다. 결과가 맞으면 승인하거나 기각하세요.",
161
+ "review.actions.aria": "검토 작업",
162
+ "review.runNow": "지금 실행",
163
+ "review.regenerated": "다시 생성됨",
164
+ "review.executed": "실행됨",
165
+ "review.approve": "승인",
166
+ "review.snoozeDay": "하루 일시 중지",
167
+ "review.dismiss": "기각",
168
+ "review.feedback.open": "승인 또는 기각 전까지 항목은 열린 상태로 유지됩니다.",
169
+ "review.action.failed": "{action} 실패",
112
170
  "admin.back": "Brain",
113
171
  "admin.aria.console": "Lattice 관리자",
114
172
  "admin.kicker": "분리된 관리자 작업공간",
@@ -372,6 +430,64 @@ export const COPY: Record<Language, TextMap> = {
372
430
  "care.result.path": "File: {path}",
373
431
  "care.result.contents": "{memories} memories · {links} links · {conversations} conversations",
374
432
  "care.result.dryRun": "Preview only; your current Brain was not changed.",
433
+ "capture.tab.files": "Files",
434
+ "capture.tab.local": "Folders",
435
+ "capture.tab.browser": "Web",
436
+ "capture.tab.pipeline": "Flow",
437
+ "capture.kicker": "Add",
438
+ "capture.title": "Feed the brain what matters.",
439
+ "capture.body": "Drop in files, connect folders, or save a page. Lattice remembers the origin of every idea it learns.",
440
+ "capture.retry": "Retry",
441
+ "capture.files.title": "Add documents",
442
+ "capture.files.description": "Choose files and Lattice will prepare them for search and memory.",
443
+ "capture.files.drop": "Drop files or choose documents",
444
+ "capture.files.dropDetail": "Each file is queued, parsed, written to Memory, and linked into the Brain graph with source metadata.",
445
+ "capture.files.uploaded": "Uploaded documents",
446
+ "capture.files.completed": "Completed uploads appear here after they enter Memory. Graph links may continue building briefly after parsing finishes.",
447
+ "capture.files.success": "Entered Brain and graph queue",
448
+ "capture.files.queued": "Waiting in the ingest queue",
449
+ "capture.files.uploading": "Parsing and sending to Memory",
450
+ "capture.files.failed": "Ingest failed before it entered the Brain",
451
+ "capture.files.captured": "Captured with source metadata",
452
+ "capture.files.capturedWithNode": "Captured with source metadata · {node}",
453
+ "capture.local.title": "Connect a folder",
454
+ "capture.local.description": "Point Lattice at a folder you want it to remember.",
455
+ "capture.local.placeholder": "Folder path on this Mac",
456
+ "capture.local.connect": "Connect Folder",
457
+ "capture.local.success": "Folder connection requested",
458
+ "capture.local.sources": "Connected sources",
459
+ "capture.local.access": "Folder access",
460
+ "capture.browser.title": "Save a web page",
461
+ "capture.browser.description": "Capture a page so Lattice can remember the useful parts.",
462
+ "capture.browser.placeholder": "https://example.com/article",
463
+ "capture.browser.capture": "Capture URL",
464
+ "capture.browser.success": "URL capture requested",
465
+ "capture.pipeline.status": "Processing status",
466
+ "capture.pipeline.growth": "Brain growth",
467
+ "capture.pipeline.refresh": "Refresh memory",
468
+ "capture.pipeline.description": "Refresh search when you want Lattice to re-check captured material.",
469
+ "capture.pipeline.rebuild": "Rebuild retrieval index",
470
+ "review.inbox.title": "Review inbox",
471
+ "review.inbox.description": "Automation suggestions waiting for your decision. Run now previews without approving.",
472
+ "review.status.connected": "connected",
473
+ "review.status.unavailable": "unavailable",
474
+ "review.inbox.loadError": "Could not load review inbox",
475
+ "review.inbox.unavailable": "The review queue is not available right now.",
476
+ "review.inbox.empty": "Nothing to review",
477
+ "review.inbox.emptySnoozed": "Snoozed items will appear here until they are unsnoozed or become pending again.",
478
+ "review.inbox.emptyPending": "When automations opt into the review queue, new suggestions will appear here.",
479
+ "review.snoozed.detail": "This stays out of the pending queue until then. Unsnooze brings it back immediately.",
480
+ "review.unsnooze": "Unsnooze",
481
+ "review.runNow.detail": "Run now previews the action without approving it. Approve or dismiss when the result looks right.",
482
+ "review.actions.aria": "Review actions",
483
+ "review.runNow": "Run now",
484
+ "review.regenerated": "Regenerated",
485
+ "review.executed": "Executed",
486
+ "review.approve": "Approve",
487
+ "review.snoozeDay": "Snooze 1 day",
488
+ "review.dismiss": "Dismiss",
489
+ "review.feedback.open": "item stays open until you approve or dismiss.",
490
+ "review.action.failed": "{action} failed",
375
491
  "admin.back": "Brain",
376
492
  "admin.aria.console": "Lattice Admin",
377
493
  "admin.kicker": "Separate admin workspace",
@@ -6,28 +6,30 @@ import { ActionButton, DataPanel, EntityList, OperationResult, StructuredView, T
6
6
  import { Button } from "@/components/ui/button";
7
7
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
8
8
  import { Input } from "@/components/ui/input";
9
+ import { t, type Language } from "@/i18n";
9
10
  import { asArray } from "@/lib/utils";
11
+ import { useAppStore } from "@/store/appStore";
10
12
 
11
13
  type CaptureTab = "files" | "local" | "browser" | "pipeline";
12
14
 
13
- const tabs: Array<{ id: CaptureTab; label: string }> = [
14
- { id: "files", label: "Files" },
15
- { id: "local", label: "Folders" },
16
- { id: "browser", label: "Web" },
17
- { id: "pipeline", label: "Flow" },
18
- ];
19
-
20
15
  export function CapturePage({ initialTab }: { initialTab?: string }) {
16
+ const language = useAppStore((state) => state.language);
21
17
  const [tab, setTab] = React.useState<CaptureTab>((initialTab as CaptureTab) || "files");
18
+ const tabs: Array<{ id: CaptureTab; label: string }> = [
19
+ { id: "files", label: t(language, "capture.tab.files") },
20
+ { id: "local", label: t(language, "capture.tab.local") },
21
+ { id: "browser", label: t(language, "capture.tab.browser") },
22
+ { id: "pipeline", label: t(language, "capture.tab.pipeline") },
23
+ ];
22
24
  React.useEffect(() => {
23
25
  if (initialTab === "pipeline" || initialTab === "local" || initialTab === "files") setTab(initialTab);
24
26
  }, [initialTab]);
25
27
  return (
26
28
  <div className="space-y-5">
27
29
  <header className="page-hero">
28
- <div className="page-kicker"><Upload className="h-4 w-4" /> Add</div>
29
- <h1 className="page-title">Feed the brain what matters.</h1>
30
- <p className="page-copy">Drop in files, connect folders, or save a page. Lattice remembers the origin of every idea it learns.</p>
30
+ <div className="page-kicker"><Upload className="h-4 w-4" /> {t(language, "capture.kicker")}</div>
31
+ <h1 className="page-title">{t(language, "capture.title")}</h1>
32
+ <p className="page-copy">{t(language, "capture.body")}</p>
31
33
  </header>
32
34
  <Tabs tabs={tabs} value={tab} onChange={(id) => setTab(id as CaptureTab)} />
33
35
  {tab === "files" ? <FilesPanel /> : null}
@@ -39,6 +41,7 @@ export function CapturePage({ initialTab }: { initialTab?: string }) {
39
41
  }
40
42
 
41
43
  function FilesPanel() {
44
+ const language = useAppStore((state) => state.language);
42
45
  const qc = useQueryClient();
43
46
  const docs = useQuery({ queryKey: ["documents"], queryFn: () => latticeApi.documents(200) });
44
47
  const [queue, setQueue] = React.useState<UploadQueueItem[]>([]);
@@ -59,8 +62,8 @@ function FilesPanel() {
59
62
  <div className="grid gap-4 xl:grid-cols-[0.75fr_1.25fr]">
60
63
  <Card>
61
64
  <CardHeader>
62
- <CardTitle className="flex items-center gap-2"><Upload className="h-4 w-4" /> Add documents</CardTitle>
63
- <CardDescription>Choose files and Lattice will prepare them for search and memory.</CardDescription>
65
+ <CardTitle className="flex items-center gap-2"><Upload className="h-4 w-4" /> {t(language, "capture.files.title")}</CardTitle>
66
+ <CardDescription>{t(language, "capture.files.description")}</CardDescription>
64
67
  </CardHeader>
65
68
  <CardContent>
66
69
  <label
@@ -72,19 +75,19 @@ function FilesPanel() {
72
75
  }}
73
76
  >
74
77
  <Upload className="h-7 w-7 text-primary" />
75
- <span className="text-lg font-semibold">Drop files or choose documents</span>
76
- <span className="max-w-sm text-sm leading-6 text-muted-foreground">Each file is queued, parsed, written to Memory, and linked into the Brain graph with source metadata.</span>
78
+ <span className="text-lg font-semibold">{t(language, "capture.files.drop")}</span>
79
+ <span className="max-w-sm text-sm leading-6 text-muted-foreground">{t(language, "capture.files.dropDetail")}</span>
77
80
  <input type="file" multiple className="sr-only" onChange={(e) => e.target.files && beginUpload(e.target.files)} />
78
81
  </label>
79
82
  <DocumentUploadQueue queue={queue} onRetry={(file) => beginUpload([file])} />
80
83
  </CardContent>
81
84
  </Card>
82
- <DataPanel title="Uploaded documents" result={docs.data}>
85
+ <DataPanel title={t(language, "capture.files.uploaded")} result={docs.data}>
83
86
  {(data) => (
84
87
  <div className="space-y-3">
85
88
  <EntityList items={(data as Record<string, unknown>).documents || data} titleKey="filename" metaKey="ingest_state" limit={12} />
86
89
  <div className="rounded-md border border-border bg-background/55 p-3 text-sm text-muted-foreground">
87
- Completed uploads appear here after they enter Memory. Graph links may continue building briefly after parsing finishes.
90
+ {t(language, "capture.files.completed")}
88
91
  </div>
89
92
  </div>
90
93
  )}
@@ -122,11 +125,12 @@ async function uploadFiles(files: File[], setQueue: React.Dispatch<React.SetStat
122
125
  }
123
126
 
124
127
  function DocumentUploadQueue({ queue, onRetry }: { queue: UploadQueueItem[]; onRetry: (file: File) => void }) {
128
+ const language = useAppStore((state) => state.language);
125
129
  if (!queue.length) return null;
126
130
  return (
127
131
  <div className="mt-4 space-y-2">
128
132
  {queue.map((item) => {
129
- const detail = uploadResultDetail(item);
133
+ const detail = uploadResultDetail(item, language);
130
134
  return (
131
135
  <div key={item.id} className="rounded-md border border-border bg-background/55 p-3 text-sm">
132
136
  <div className="flex flex-wrap items-start justify-between gap-3">
@@ -141,11 +145,11 @@ function DocumentUploadQueue({ queue, onRetry }: { queue: UploadQueueItem[]; onR
141
145
  </div>
142
146
  {item.status === "failed" ? (
143
147
  <Button size="sm" variant="outline" onClick={() => onRetry(item.file)}>
144
- <RotateCcw className="h-3.5 w-3.5" /> Retry
148
+ <RotateCcw className="h-3.5 w-3.5" /> {t(language, "capture.retry")}
145
149
  </Button>
146
150
  ) : null}
147
151
  </div>
148
- {item.result ? <OperationResult result={item.result} successLabel="Entered Brain and graph queue" /> : null}
152
+ {item.result ? <OperationResult result={item.result} successLabel={t(language, "capture.files.success")} /> : null}
149
153
  </div>
150
154
  );
151
155
  })}
@@ -153,16 +157,17 @@ function DocumentUploadQueue({ queue, onRetry }: { queue: UploadQueueItem[]; onR
153
157
  );
154
158
  }
155
159
 
156
- function uploadResultDetail(item: UploadQueueItem) {
157
- if (item.status === "queued") return "Waiting in the ingest queue";
158
- if (item.status === "uploading") return "Parsing and sending to Memory";
159
- if (!item.result?.ok) return item.result?.error || "Ingest failed before it entered the Brain";
160
+ function uploadResultDetail(item: UploadQueueItem, language: Language) {
161
+ if (item.status === "queued") return t(language, "capture.files.queued");
162
+ if (item.status === "uploading") return t(language, "capture.files.uploading");
163
+ if (!item.result?.ok) return item.result?.error || t(language, "capture.files.failed");
160
164
  const data = item.result.data || {};
161
165
  const node = String(data.node_id || data.graph_node || data.provenance_id || "");
162
- return node ? `Captured with source metadata · ${node}` : "Captured with source metadata";
166
+ return node ? t(language, "capture.files.capturedWithNode", { node }) : t(language, "capture.files.captured");
163
167
  }
164
168
 
165
169
  function LocalPanel() {
170
+ const language = useAppStore((state) => state.language);
166
171
  const qc = useQueryClient();
167
172
  const [path, setPath] = React.useState("");
168
173
  const local = useQuery({ queryKey: ["localSources"], queryFn: latticeApi.localSources });
@@ -175,16 +180,16 @@ function LocalPanel() {
175
180
  <div className="grid gap-4 xl:grid-cols-[0.9fr_1.1fr]">
176
181
  <Card>
177
182
  <CardHeader>
178
- <CardTitle className="flex items-center gap-2"><FolderPlus className="h-4 w-4" /> Connect a folder</CardTitle>
179
- <CardDescription>Point Lattice at a folder you want it to remember.</CardDescription>
183
+ <CardTitle className="flex items-center gap-2"><FolderPlus className="h-4 w-4" /> {t(language, "capture.local.title")}</CardTitle>
184
+ <CardDescription>{t(language, "capture.local.description")}</CardDescription>
180
185
  </CardHeader>
181
186
  <CardContent className="space-y-3">
182
- <Input value={path} onChange={(e) => setPath(e.target.value)} placeholder="Folder path on this Mac" />
183
- <Button disabled={!path.trim() || connect.isPending} onClick={() => connect.mutate()}>Connect Folder</Button>
184
- {connect.data ? <OperationResult result={connect.data} successLabel="Folder connection requested" /> : null}
187
+ <Input value={path} onChange={(e) => setPath(e.target.value)} placeholder={t(language, "capture.local.placeholder")} />
188
+ <Button disabled={!path.trim() || connect.isPending} onClick={() => connect.mutate()}>{t(language, "capture.local.connect")}</Button>
189
+ {connect.data ? <OperationResult result={connect.data} successLabel={t(language, "capture.local.success")} /> : null}
185
190
  </CardContent>
186
191
  </Card>
187
- <DataPanel title="Connected sources" result={local.data}>
192
+ <DataPanel title={t(language, "capture.local.sources")} result={local.data}>
188
193
  {(data) => (
189
194
  <div className="space-y-3">
190
195
  <EntityList items={(data as Record<string, unknown>).sources} titleKey="path" metaKey="status" />
@@ -199,7 +204,7 @@ function LocalPanel() {
199
204
  </div>
200
205
  )}
201
206
  </DataPanel>
202
- <DataPanel title="Folder access" result={agent.data} className="xl:col-span-2">
207
+ <DataPanel title={t(language, "capture.local.access")} result={agent.data} className="xl:col-span-2">
203
208
  {(data) => <StructuredView value={data} />}
204
209
  </DataPanel>
205
210
  </div>
@@ -207,43 +212,45 @@ function LocalPanel() {
207
212
  }
208
213
 
209
214
  function BrowserPanel() {
215
+ const language = useAppStore((state) => state.language);
210
216
  const [url, setUrl] = React.useState("");
211
217
  const read = useMutation({ mutationFn: () => latticeApi.browserReadUrl(url) });
212
218
  return (
213
219
  <Card>
214
220
  <CardHeader>
215
- <CardTitle className="flex items-center gap-2"><Globe2 className="h-4 w-4" /> Save a web page</CardTitle>
216
- <CardDescription>Capture a page so Lattice can remember the useful parts.</CardDescription>
221
+ <CardTitle className="flex items-center gap-2"><Globe2 className="h-4 w-4" /> {t(language, "capture.browser.title")}</CardTitle>
222
+ <CardDescription>{t(language, "capture.browser.description")}</CardDescription>
217
223
  </CardHeader>
218
224
  <CardContent className="space-y-3">
219
225
  <div className="flex flex-col gap-2 sm:flex-row">
220
- <Input value={url} onChange={(e) => setUrl(e.target.value)} placeholder="https://example.com/article" />
221
- <Button disabled={!url.trim() || read.isPending} onClick={() => read.mutate()}>Capture URL</Button>
226
+ <Input value={url} onChange={(e) => setUrl(e.target.value)} placeholder={t(language, "capture.browser.placeholder")} />
227
+ <Button disabled={!url.trim() || read.isPending} onClick={() => read.mutate()}>{t(language, "capture.browser.capture")}</Button>
222
228
  </div>
223
- {read.data ? <OperationResult result={read.data} successLabel="URL capture requested" /> : null}
229
+ {read.data ? <OperationResult result={read.data} successLabel={t(language, "capture.browser.success")} /> : null}
224
230
  </CardContent>
225
231
  </Card>
226
232
  );
227
233
  }
228
234
 
229
235
  function PipelinePanel() {
236
+ const language = useAppStore((state) => state.language);
230
237
  const index = useQuery({ queryKey: ["index"], queryFn: latticeApi.indexStatus });
231
238
  const stats = useQuery({ queryKey: ["graphStats"], queryFn: latticeApi.graphStats });
232
239
  return (
233
240
  <div className="grid gap-4 xl:grid-cols-2">
234
- <DataPanel title="Processing status" result={index.data}>
241
+ <DataPanel title={t(language, "capture.pipeline.status")} result={index.data}>
235
242
  {(data) => <StructuredView value={data} />}
236
243
  </DataPanel>
237
- <DataPanel title="Brain growth" result={stats.data}>
244
+ <DataPanel title={t(language, "capture.pipeline.growth")} result={stats.data}>
238
245
  {(data) => <StructuredView value={data} />}
239
246
  </DataPanel>
240
247
  <Card className="xl:col-span-2">
241
248
  <CardHeader>
242
- <CardTitle className="flex items-center gap-2"><HardDrive className="h-4 w-4" /> Refresh memory</CardTitle>
243
- <CardDescription>Refresh search when you want Lattice to re-check captured material.</CardDescription>
249
+ <CardTitle className="flex items-center gap-2"><HardDrive className="h-4 w-4" /> {t(language, "capture.pipeline.refresh")}</CardTitle>
250
+ <CardDescription>{t(language, "capture.pipeline.description")}</CardDescription>
244
251
  </CardHeader>
245
252
  <CardContent>
246
- <ActionButton label="Rebuild retrieval index" action={() => latticeApi.rebuildIndex()} invalidate={["index"]} />
253
+ <ActionButton label={t(language, "capture.pipeline.rebuild")} action={() => latticeApi.rebuildIndex()} invalidate={["index"]} />
247
254
  </CardContent>
248
255
  </Card>
249
256
  </div>