total-recall-brain 3.13.0 → 3.13.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.
Files changed (152) hide show
  1. package/README.md +91 -108
  2. package/bin/antigravity.mjs +81 -52
  3. package/bin/total-recall.mjs +32 -57
  4. package/extension/background.js +1 -1
  5. package/extension/options/options.js +9 -3
  6. package/extension/sidepanel/sidepanel.js +0 -2
  7. package/frontend/index.html +4 -3
  8. package/frontend/package.json +1 -0
  9. package/frontend/src/App.tsx +109 -152
  10. package/frontend/src/api.ts +605 -23
  11. package/frontend/src/components/BrainSelector.tsx +211 -199
  12. package/frontend/src/components/DocumentEditorModal.tsx +57 -0
  13. package/frontend/src/components/DocumentTable.tsx +47 -0
  14. package/frontend/src/components/Graph3D.tsx +120 -71
  15. package/frontend/src/components/UsageChart.tsx +3 -3
  16. package/frontend/src/components/brand/BrandMark.tsx +95 -0
  17. package/frontend/src/index.css +286 -86
  18. package/frontend/src/pages/ApiKeysPage.tsx +1878 -215
  19. package/frontend/src/pages/ChatPage.tsx +103 -45
  20. package/frontend/src/pages/DesignDocsPage.tsx +5 -5
  21. package/frontend/src/pages/FilesPage.tsx +10 -6
  22. package/frontend/src/pages/GraphPage.tsx +5 -3
  23. package/frontend/src/pages/HelpPage.tsx +2 -2
  24. package/frontend/src/pages/InboxPage.tsx +82 -0
  25. package/frontend/src/pages/InstructionsPage.tsx +1 -1
  26. package/frontend/src/pages/IntegrationsPage.tsx +21 -10
  27. package/frontend/src/pages/LoginPage.tsx +95 -77
  28. package/frontend/src/pages/MemoryPage.tsx +31 -8
  29. package/frontend/src/pages/OnboardingPage.tsx +690 -0
  30. package/frontend/src/pages/OpenWikiPage.tsx +412 -323
  31. package/frontend/src/pages/SettingsPage.tsx +825 -922
  32. package/frontend/src/pages/SkillsPage.tsx +272 -147
  33. package/frontend/src/pages/UsagePage.tsx +23 -31
  34. package/frontend/src/pages/VaultPage.tsx +223 -0
  35. package/frontend/src/types.ts +13 -0
  36. package/package.json +4 -4
  37. package/scaffold/.agent/skills/total-recall/SKILL.md +43 -4
  38. package/scaffold/.agent/skills/total-recall/modules/README.md +10 -0
  39. package/scaffold/.agent/skills/total-recall/modules/agents/README.md +5 -0
  40. package/scaffold/.agent/skills/total-recall/{skills/tr-cli-agents → modules/agents}/agents.yml +8 -0
  41. package/scaffold/.agent/skills/total-recall/modules/skill-deploy/README.md +9 -0
  42. package/scaffold/.agent/skills/total-recall/openwiki/README.md +10 -0
  43. package/scaffold/.agent/skills/total-recall/openwiki/architecture.md +5 -0
  44. package/scaffold/.agent/skills/total-recall/openwiki/ide.md +10 -0
  45. package/scaffold/.agent/skills/total-recall/openwiki/memory.md +11 -0
  46. package/scaffold/.agent/skills/total-recall/openwiki/secrets.md +7 -0
  47. package/scaffold/.agent/skills/total-recall/openwiki/skills.md +7 -0
  48. package/src/cli/brain.mjs +63 -5
  49. package/src/cli/connect.mjs +11 -19
  50. package/src/cli/daemon.mjs +5 -4
  51. package/src/cli/deploy-ui.mjs +8 -3
  52. package/src/cli/deploy.mjs +84 -27
  53. package/src/cli/ingest/index.mjs +1 -1
  54. package/src/cli/init.mjs +63 -34
  55. package/src/cli/remember.mjs +3 -3
  56. package/src/cli/secret.mjs +812 -0
  57. package/src/cli/setup.mjs +34 -7
  58. package/src/cli/share.mjs +2 -2
  59. package/src/cli/skill.mjs +396 -35
  60. package/src/cli/sync.mjs +4 -4
  61. package/src/cli/task.mjs +287 -0
  62. package/src/core/clarity-rewriter.mjs +5 -5
  63. package/src/core/config.mjs +44 -2
  64. package/src/core/conflict-detector.mjs +5 -5
  65. package/src/core/crons.mjs +54 -0
  66. package/src/core/daemon-loop.mjs +87 -235
  67. package/src/core/dream.mjs +170 -10
  68. package/src/core/env-import.mjs +456 -0
  69. package/src/core/fact-seeker.mjs +14 -14
  70. package/src/core/import-rules.mjs +20 -7
  71. package/src/core/inference-engine.mjs +3 -3
  72. package/src/core/migrate.mjs +14 -8
  73. package/src/core/okf-adapter.mjs +2 -2
  74. package/src/core/operation-validator.mjs +39 -331
  75. package/src/core/optimizer.mjs +7 -0
  76. package/src/core/post-mortem.mjs +2 -2
  77. package/src/core/project-brain.mjs +349 -0
  78. package/src/core/provider-catalog.mjs +296 -0
  79. package/src/core/quick-capture.mjs +1 -1
  80. package/src/core/remote-vault-sync.mjs +194 -0
  81. package/src/core/research-queue.mjs +7 -11
  82. package/src/core/runtime.mjs +33 -9
  83. package/src/core/scheduler.mjs +59 -5
  84. package/src/core/schema.mjs +14 -0
  85. package/src/core/search.mjs +1 -1
  86. package/src/core/secrets-env-export.mjs +323 -0
  87. package/src/core/secrets-rotate.mjs +245 -0
  88. package/src/core/secrets-store.mjs +645 -0
  89. package/src/core/semantic-index.mjs +3 -3
  90. package/src/core/skills-registry.mjs +1062 -0
  91. package/src/core/source-adapters.mjs +4 -1
  92. package/src/core/ssss-host-extension.mjs +94 -0
  93. package/src/core/ssss-kernel-bridge.mjs +530 -0
  94. package/src/core/surface.mjs +14 -0
  95. package/src/core/task-envelope.mjs +298 -0
  96. package/src/core/task-executors.mjs +600 -0
  97. package/src/core/validated-write.mjs +124 -56
  98. package/src/core/vault.mjs +21 -34
  99. package/src/core/webauthn-store.mjs +272 -0
  100. package/src/server/api.mjs +26 -16
  101. package/src/server/auth.mjs +51 -0
  102. package/src/server/index.mjs +3 -3
  103. package/src/server/keys.mjs +30 -4
  104. package/src/server/rest.mjs +198 -25
  105. package/src/server/route-manifest.json +156 -0
  106. package/src/server/routes/_shared.mjs +40 -1
  107. package/src/server/routes/docs.mjs +260 -0
  108. package/src/server/routes/memory.mjs +145 -32
  109. package/src/server/routes/research.mjs +30 -0
  110. package/src/server/routes/secrets.mjs +470 -0
  111. package/src/server/routes/share.mjs +2 -2
  112. package/src/server/routes/skills.mjs +452 -54
  113. package/src/server/routes/sync.mjs +48 -0
  114. package/src/server/routes/webauthn.mjs +184 -0
  115. package/src/server/update-route-manifest.mjs +39 -0
  116. package/templates/onboarding-interview.md +32 -22
  117. package/templates/openwiki/README.md +10 -0
  118. package/templates/openwiki/architecture.md +5 -0
  119. package/templates/openwiki/ide.md +10 -0
  120. package/templates/openwiki/memory.md +11 -0
  121. package/templates/openwiki/secrets.md +7 -0
  122. package/templates/openwiki/skills.md +7 -0
  123. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/SKILL.md +0 -121
  124. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/antigravity.txt +0 -20
  125. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/claude.txt +0 -58
  126. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/codex.txt +0 -81
  127. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/references/gemini.txt +0 -44
  128. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/scripts/.gitkeep +0 -0
  129. package/scaffold/.agent/skills/total-recall/skills/tr-cli-agents/subagents/.gitkeep +0 -0
  130. package/scaffold/.agent/skills/total-recall/skills/tr-research/SKILL.md +0 -338
  131. package/scaffold/.agent/skills/total-recall/skills/tr-skill/SKILL.md +0 -282
  132. package/scaffold/.agent/skills/total-recall/skills/tr-skill/assets/skill-template/SKILL.md +0 -20
  133. package/scaffold/.agent/skills/total-recall/skills/tr-skill/evals/evals.json +0 -20
  134. package/scaffold/.agent/skills/total-recall/skills/tr-skill/hooks/on-skill-add.sh +0 -14
  135. package/scaffold/.agent/skills/total-recall/skills/tr-skill/hooks/on-skill-delete.sh +0 -14
  136. package/scaffold/.agent/skills/total-recall/skills/tr-skill/learnings/.gitkeep +0 -0
  137. package/scaffold/.agent/skills/total-recall/skills/tr-skill/references/.gitkeep +0 -0
  138. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/.gitkeep +0 -0
  139. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/create-skill.sh +0 -65
  140. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/enforce-skill-optimization.mjs +0 -135
  141. package/scaffold/.agent/skills/total-recall/skills/tr-skill/scripts/skill-watcher.mjs +0 -64
  142. package/scaffold/.agent/skills/total-recall/skills/tr-skill/subagents/audit-all-skills.md +0 -62
  143. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/SKILL.md +0 -298
  144. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/evals/evals.json +0 -17
  145. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/references/CHANGELOG.md +0 -124
  146. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/references/ssss-spec.md +0 -1325
  147. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/scripts/validate-schema.mjs +0 -232
  148. package/scaffold/.agent/skills/total-recall/skills/tr-ssss/subagents/audit-ssss-node.md +0 -9
  149. /package/scaffold/.agent/skills/total-recall/{skills/tr-cli-agents/evals → modules/skill-deploy/scripts}/.gitkeep +0 -0
  150. /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/find-skills.mjs +0 -0
  151. /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/install-skill.mjs +0 -0
  152. /package/scaffold/.agent/skills/total-recall/{skills/tr-skill → modules/skill-deploy}/scripts/scan-skill.mjs +0 -0
@@ -1,278 +1,290 @@
1
- import { useState, useEffect, useRef } from 'react';
2
- import { getApiBase } from '../api';
1
+ import { useState, useEffect, useRef } from 'react'
2
+ import { getApiBase } from '../api'
3
3
 
4
4
  interface BrainInfo {
5
- id: string;
6
- name: string;
7
- layer: 'global' | 'project';
8
- path: string;
9
- project_root?: string;
10
- exists: boolean;
11
- node_count: number;
12
- registered_at?: string;
13
- last_compiled: string | null;
5
+ id: string
6
+ name: string
7
+ layer: 'global' | 'project'
8
+ path: string
9
+ project_root?: string
10
+ exists: boolean
11
+ node_count: number
12
+ registered_at?: string
13
+ last_compiled: string | null
14
14
  }
15
15
 
16
16
  interface BrainSelectorProps {
17
- activeBrainId: string;
18
- onBrainChange: (brainId: string) => void;
17
+ activeBrainId: string
18
+ onBrainChange: (brainId: string) => void
19
+ }
20
+
21
+ function layerDot(layer: string, multi = false): string {
22
+ if (multi) return 'linear-gradient(135deg, #60a5fa, #34d399)'
23
+ if (layer === 'global') return 'linear-gradient(135deg, #60a5fa, #2563eb)'
24
+ return 'linear-gradient(135deg, #34d399, #059669)'
19
25
  }
20
26
 
21
27
  export default function BrainSelector({ activeBrainId, onBrainChange }: BrainSelectorProps) {
22
- const [brains, setBrains] = useState<BrainInfo[]>([]);
23
- const [expanded, setExpanded] = useState(false);
24
- const [loading, setLoading] = useState(true);
25
- const selectorRef = useRef<HTMLDivElement>(null);
28
+ const [brains, setBrains] = useState<BrainInfo[]>([])
29
+ const [expanded, setExpanded] = useState(false)
30
+ const [loading, setLoading] = useState(true)
31
+ const selectorRef = useRef<HTMLDivElement>(null)
26
32
 
27
33
  useEffect(() => {
28
- const base = getApiBase();
34
+ const base = getApiBase()
29
35
  fetch(`${base}/api/brains`, { credentials: 'include' })
30
- .then(r => r.json())
31
- .then(data => {
32
- setBrains(data.brains || []);
33
- setLoading(false);
36
+ .then((r) => r.json())
37
+ .then((data) => {
38
+ setBrains(data.brains || [])
39
+ setLoading(false)
34
40
  })
35
- .catch(() => setLoading(false));
36
- }, []);
41
+ .catch(() => setLoading(false))
42
+ }, [])
37
43
 
38
44
  useEffect(() => {
39
45
  function handleClickOutside(event: MouseEvent) {
40
46
  if (selectorRef.current && !selectorRef.current.contains(event.target as Node)) {
41
- setExpanded(false);
47
+ setExpanded(false)
42
48
  }
43
49
  }
44
- document.addEventListener("mousedown", handleClickOutside);
45
- return () => {
46
- document.removeEventListener("mousedown", handleClickOutside);
47
- };
48
- }, []);
49
-
50
- const selectedIds = activeBrainId.split(',').filter(Boolean);
51
- const selectedBrains = brains.filter(b => selectedIds.includes(b.id));
52
-
53
- // Fallback to first brain if none selected
54
- const activeBrain = selectedBrains.length > 0 ? selectedBrains[0] : brains[0];
55
-
56
- const totalNodes = selectedBrains.length > 0
57
- ? selectedBrains.reduce((sum, b) => sum + b.node_count, 0)
58
- : (activeBrain?.node_count || 0);
59
-
60
- const concatenatedNames = selectedBrains.length > 0
61
- ? selectedBrains.map(b => b.name).join(' + ')
62
- : (activeBrain?.name || 'No Brain');
63
-
64
- const layerText = selectedBrains.length > 0
65
- ? selectedBrains.map(b => b.layer).filter((v, i, a) => a.indexOf(v) === i).join('/')
66
- : (activeBrain?.layer || 'global');
67
-
68
- const isMulti = selectedIds.length > 1;
50
+ document.addEventListener('mousedown', handleClickOutside)
51
+ return () => document.removeEventListener('mousedown', handleClickOutside)
52
+ }, [])
69
53
 
70
- const dotBg = isMulti
71
- ? 'linear-gradient(135deg, #a855f7, #6366f1, #10b981)'
72
- : activeBrain?.layer === 'global'
73
- ? 'linear-gradient(135deg, #818cf8, #6366f1)'
74
- : 'linear-gradient(135deg, #34d399, #10b981)';
54
+ const selectedIds = activeBrainId.split(',').filter(Boolean)
55
+ const selectedBrains = brains.filter((b) => selectedIds.includes(b.id))
56
+ const primary = selectedBrains[0] || brains[0]
57
+ const isMulti = selectedIds.length > 1
58
+ const totalNodes = selectedBrains.reduce((sum, b) => sum + (b.node_count || 0), 0) || primary?.node_count || 0
75
59
 
76
- const dotShadow = isMulti
77
- ? '0 0 8px rgba(168,85,247,0.6)'
78
- : activeBrain?.layer === 'global'
79
- ? '0 0 6px rgba(99,102,241,0.5)'
80
- : '0 0 6px rgba(16,185,129,0.5)';
60
+ const label = isMulti
61
+ ? `${selectedIds.length} brains`
62
+ : primary?.name || 'Brain'
81
63
 
82
- const handleRowClick = (id: string, e: React.MouseEvent) => {
83
- e.preventDefault();
84
- e.stopPropagation();
85
- onBrainChange(id);
86
- };
87
-
88
- const handleToggle = (id: string, e?: React.SyntheticEvent) => {
89
- if (e) {
90
- e.stopPropagation();
91
- }
64
+ const handleToggle = (id: string, e: React.SyntheticEvent) => {
65
+ e.stopPropagation()
92
66
  if (selectedIds.includes(id)) {
93
- const nextIds = selectedIds.filter(x => x !== id);
94
- onBrainChange(nextIds.join(','));
67
+ onBrainChange(selectedIds.filter((x) => x !== id).join(','))
95
68
  } else {
96
- const nextIds = [...selectedIds, id];
97
- onBrainChange(nextIds.join(','));
69
+ onBrainChange([...selectedIds, id].join(','))
98
70
  }
99
- };
71
+ }
100
72
 
101
73
  if (loading) {
102
74
  return (
103
- <div style={{
104
- padding: '8px 10px',
105
- background: 'var(--bg-tertiary)',
106
- borderRadius: 8,
107
- fontSize: 11,
108
- color: 'var(--text-tertiary)',
109
- display: 'flex',
110
- alignItems: 'center',
111
- gap: 6,
112
- }}>
113
- <span style={{ animation: 'spin 0.8s linear infinite', display: 'inline-block' }}>⟳</span>
114
- Loading brains…
75
+ <div className="brain-selector-trigger" style={{ opacity: 0.6 }}>
76
+ <span style={{ fontSize: 11, color: 'var(--text-tertiary)' }}>Brains…</span>
115
77
  </div>
116
- );
78
+ )
117
79
  }
118
80
 
119
- if (brains.length === 0) return null;
81
+ if (brains.length === 0) return null
120
82
 
121
83
  return (
122
- <div ref={selectorRef} style={{ position: 'relative' }}>
123
- {/* Active brain pill */}
84
+ <div ref={selectorRef} className="brain-selector" style={{ position: 'relative' }}>
124
85
  <button
125
- onClick={() => setExpanded(!expanded)}
86
+ type="button"
87
+ className="brain-selector-trigger"
88
+ onClick={() => setExpanded((v) => !v)}
89
+ title="Select brain layers"
126
90
  style={{
127
91
  width: '100%',
128
- padding: '8px 10px',
129
- background: expanded ? 'var(--accent-faint)' : 'var(--bg-tertiary)',
130
- border: `1px solid ${expanded ? 'var(--accent)' : 'var(--border)'}`,
131
- borderRadius: 8,
132
- cursor: 'pointer',
133
- color: 'var(--text-primary)',
134
92
  display: 'flex',
135
93
  alignItems: 'center',
136
94
  gap: 8,
137
- fontSize: 12,
138
- transition: 'all 0.15s',
95
+ padding: '6px 8px',
96
+ background: expanded ? 'rgba(59, 130, 246, 0.12)' : 'rgba(148, 163, 184, 0.06)',
97
+ border: `1px solid ${expanded ? 'var(--border-accent)' : 'var(--border)'}`,
98
+ borderRadius: 8,
99
+ cursor: 'pointer',
100
+ color: 'var(--text-primary)',
139
101
  textAlign: 'left',
102
+ transition: 'all 0.15s',
140
103
  }}
141
- title="Switch brain layer"
142
104
  >
143
- <span style={{
144
- width: 8, height: 8, borderRadius: '50%',
145
- background: dotBg,
146
- flexShrink: 0,
147
- boxShadow: dotShadow,
148
- }} />
149
- <div style={{ flex: 1, minWidth: 0 }}>
150
- <div style={{ fontWeight: 600, fontSize: 11, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
151
- {concatenatedNames}
152
- </div>
153
- <div style={{ fontSize: 10, color: 'var(--text-tertiary)', marginTop: 1 }}>
154
- {totalNodes} nodes · {layerText}
155
- </div>
156
- </div>
157
- <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5"
158
- style={{ transform: expanded ? 'rotate(180deg)' : 'rotate(0)', transition: 'transform 0.15s', flexShrink: 0, opacity: 0.5 }}>
105
+ <span
106
+ style={{
107
+ width: 7,
108
+ height: 7,
109
+ borderRadius: '50%',
110
+ background: layerDot(primary?.layer || 'global', isMulti),
111
+ boxShadow: isMulti
112
+ ? '0 0 6px rgba(52, 211, 153, 0.5)'
113
+ : '0 0 6px rgba(59, 130, 246, 0.45)',
114
+ flexShrink: 0,
115
+ }}
116
+ />
117
+ <span
118
+ style={{
119
+ flex: 1,
120
+ minWidth: 0,
121
+ fontSize: 11,
122
+ fontWeight: 600,
123
+ letterSpacing: '-0.01em',
124
+ overflow: 'hidden',
125
+ textOverflow: 'ellipsis',
126
+ whiteSpace: 'nowrap',
127
+ }}
128
+ >
129
+ {label}
130
+ </span>
131
+ <span style={{ fontSize: 10, color: 'var(--text-tertiary)', flexShrink: 0 }}>
132
+ {totalNodes}
133
+ </span>
134
+ <svg
135
+ width="10"
136
+ height="10"
137
+ viewBox="0 0 24 24"
138
+ fill="none"
139
+ stroke="currentColor"
140
+ strokeWidth="2.5"
141
+ style={{
142
+ opacity: 0.45,
143
+ transform: expanded ? 'rotate(180deg)' : 'none',
144
+ transition: 'transform 0.15s',
145
+ flexShrink: 0,
146
+ }}
147
+ >
159
148
  <polyline points="6 9 12 15 18 9" />
160
149
  </svg>
161
150
  </button>
162
151
 
163
- {/* Dropdown */}
164
152
  {expanded && (
165
- <div style={{
166
- position: 'absolute',
167
- bottom: '100%',
168
- left: 0,
169
- right: 0,
170
- marginBottom: 4,
171
- background: 'var(--bg-secondary)',
172
- border: '1px solid var(--border)',
173
- borderRadius: 8,
174
- boxShadow: '0 -4px 20px rgba(0,0,0,0.3)',
175
- overflow: 'hidden',
176
- zIndex: 1000,
177
- }}>
178
- <div style={{ padding: '8px 10px', fontSize: 10, fontWeight: 700, color: 'var(--text-tertiary)', textTransform: 'uppercase', letterSpacing: 1 }}>
179
- Brain Layers
153
+ <div
154
+ className="brain-selector-menu"
155
+ style={{
156
+ position: 'absolute',
157
+ bottom: '100%',
158
+ left: 0,
159
+ right: 0,
160
+ marginBottom: 6,
161
+ background: 'linear-gradient(165deg, rgba(18, 26, 43, 0.98), rgba(8, 12, 22, 0.99))',
162
+ border: '1px solid var(--border)',
163
+ borderRadius: 10,
164
+ boxShadow: '0 -8px 28px rgba(0,0,0,0.45)',
165
+ overflow: 'hidden',
166
+ zIndex: 1000,
167
+ maxHeight: 280,
168
+ overflowY: 'auto',
169
+ }}
170
+ >
171
+ <div
172
+ style={{
173
+ padding: '6px 10px',
174
+ fontSize: 9,
175
+ fontWeight: 700,
176
+ color: 'var(--text-tertiary)',
177
+ textTransform: 'uppercase',
178
+ letterSpacing: '0.08em',
179
+ borderBottom: '1px solid var(--border)',
180
+ }}
181
+ >
182
+ Brains · multi-select
180
183
  </div>
181
- {brains.map(brain => {
182
- const isSelected = selectedIds.includes(brain.id);
184
+ {brains.map((brain) => {
185
+ const isSelected = selectedIds.includes(brain.id)
183
186
  return (
184
187
  <div
185
188
  key={brain.id}
186
- onClick={(e) => {
187
- if (brain.exists) {
188
- handleRowClick(brain.id, e);
189
- setExpanded(false); // Close dropdown on single selection
189
+ role="button"
190
+ tabIndex={0}
191
+ onClick={() => {
192
+ if (!brain.exists) return
193
+ onBrainChange(brain.id)
194
+ setExpanded(false)
195
+ }}
196
+ onKeyDown={(e) => {
197
+ if (e.key === 'Enter' && brain.exists) {
198
+ onBrainChange(brain.id)
199
+ setExpanded(false)
190
200
  }
191
201
  }}
192
202
  style={{
193
- width: '100%',
194
- padding: '8px 10px',
195
- background: isSelected ? 'var(--accent-faint)' : 'transparent',
196
- border: 'none',
197
- cursor: brain.exists ? 'pointer' : 'not-allowed',
198
- color: brain.exists ? 'var(--text-primary)' : 'var(--text-tertiary)',
199
203
  display: 'flex',
200
204
  alignItems: 'center',
201
205
  gap: 8,
202
- fontSize: 12,
203
- textAlign: 'left',
204
- opacity: brain.exists ? 1 : 0.5,
206
+ padding: '7px 10px',
207
+ cursor: brain.exists ? 'pointer' : 'not-allowed',
208
+ opacity: brain.exists ? 1 : 0.45,
209
+ background: isSelected ? 'rgba(59, 130, 246, 0.12)' : 'transparent',
210
+ borderLeft: isSelected ? '2px solid #3b82f6' : '2px solid transparent',
205
211
  transition: 'background 0.1s',
206
212
  }}
207
- onMouseEnter={e => { if (brain.exists) (e.currentTarget as HTMLElement).style.background = 'var(--bg-tertiary)' }}
208
- onMouseLeave={e => { (e.currentTarget as HTMLElement).style.background = isSelected ? 'var(--accent-faint)' : 'transparent' }}
209
- role="button"
210
- tabIndex={0}
213
+ onMouseEnter={(e) => {
214
+ if (brain.exists) e.currentTarget.style.background = 'rgba(148, 163, 184, 0.08)'
215
+ }}
216
+ onMouseLeave={(e) => {
217
+ e.currentTarget.style.background = isSelected ? 'rgba(59, 130, 246, 0.12)' : 'transparent'
218
+ }}
211
219
  >
212
220
  <input
213
221
  type="checkbox"
214
222
  checked={isSelected}
215
223
  disabled={!brain.exists}
224
+ onClick={(e) => e.stopPropagation()}
225
+ onChange={(e) => handleToggle(brain.id, e)}
216
226
  style={{
217
- accentColor: 'var(--accent)',
227
+ width: 13,
228
+ height: 13,
229
+ accentColor: '#3b82f6',
218
230
  cursor: brain.exists ? 'pointer' : 'not-allowed',
219
- marginRight: 4,
231
+ margin: 0,
232
+ flexShrink: 0,
233
+ }}
234
+ />
235
+ <span
236
+ style={{
237
+ width: 6,
238
+ height: 6,
239
+ borderRadius: '50%',
240
+ background: layerDot(brain.layer),
241
+ flexShrink: 0,
220
242
  }}
221
- onClick={(e) => e.stopPropagation()}
222
- onChange={(e) => handleToggle(brain.id, e)}
223
243
  />
224
- <span style={{
225
- width: 8, height: 8, borderRadius: '50%',
226
- background: brain.layer === 'global'
227
- ? 'linear-gradient(135deg, #818cf8, #6366f1)'
228
- : 'linear-gradient(135deg, #34d399, #10b981)',
229
- flexShrink: 0,
230
- }} />
231
244
  <div style={{ flex: 1, minWidth: 0 }}>
232
- <div style={{ fontWeight: 600, fontSize: 11 }}>
245
+ <div
246
+ style={{
247
+ fontSize: 11,
248
+ fontWeight: 600,
249
+ color: 'var(--text-primary)',
250
+ overflow: 'hidden',
251
+ textOverflow: 'ellipsis',
252
+ whiteSpace: 'nowrap',
253
+ }}
254
+ >
233
255
  {brain.name}
234
256
  </div>
235
- <div style={{ fontSize: 10, color: 'var(--text-tertiary)', marginTop: 1 }}>
236
- {brain.node_count} nodes
237
- {brain.layer === 'project' && brain.project_root && (
238
- <span> · {brain.project_root.split('/').pop()}</span>
239
- )}
240
- {!brain.exists && ' · not found'}
257
+ <div style={{ fontSize: 9, color: 'var(--text-tertiary)', marginTop: 1 }}>
258
+ {brain.node_count}n
259
+ {brain.layer === 'project' && brain.project_root
260
+ ? ` · ${brain.project_root.split('/').pop()}`
261
+ : ''}
262
+ {!brain.exists ? ' · missing' : ''}
241
263
  </div>
242
264
  </div>
243
- <span style={{
244
- fontSize: 9,
245
- padding: '1px 5px',
246
- borderRadius: 4,
247
- background: brain.layer === 'global'
248
- ? 'rgba(99,102,241,0.15)'
249
- : 'rgba(16,185,129,0.15)',
250
- color: brain.layer === 'global' ? '#818cf8' : '#34d399',
251
- fontWeight: 600,
252
- flexShrink: 0,
253
- }}>
254
- {brain.layer}
265
+ <span
266
+ style={{
267
+ fontSize: 8,
268
+ fontWeight: 700,
269
+ textTransform: 'uppercase',
270
+ letterSpacing: '0.04em',
271
+ padding: '2px 5px',
272
+ borderRadius: 4,
273
+ flexShrink: 0,
274
+ background:
275
+ brain.layer === 'global'
276
+ ? 'rgba(59, 130, 246, 0.15)'
277
+ : 'rgba(16, 185, 129, 0.12)',
278
+ color: brain.layer === 'global' ? '#93c5fd' : '#6ee7b7',
279
+ }}
280
+ >
281
+ {brain.layer === 'global' ? 'G' : 'P'}
255
282
  </span>
256
283
  </div>
257
- );
284
+ )
258
285
  })}
259
286
  </div>
260
287
  )}
261
-
262
- {/* Empty state when zero brains are selected */}
263
- {selectedIds.length === 0 && !expanded && (
264
- <div style={{
265
- marginTop: 6,
266
- padding: '8px 10px',
267
- background: 'var(--bg-tertiary)',
268
- borderRadius: 8,
269
- fontSize: 11,
270
- color: 'var(--text-tertiary)',
271
- textAlign: 'center',
272
- }}>
273
- No brain selected. Select a brain to view its memories.
274
- </div>
275
- )}
276
288
  </div>
277
- );
289
+ )
278
290
  }
@@ -0,0 +1,57 @@
1
+ // React import removed
2
+
3
+ export interface DocumentEditorModalProps {
4
+ isNew: boolean;
5
+ editingPath: string;
6
+ newPath: string;
7
+ editContent: string;
8
+ saving: boolean;
9
+ onPathChange: (val: string) => void;
10
+ onContentChange: (val: string) => void;
11
+ onSave: () => void;
12
+ onCancel: () => void;
13
+ }
14
+
15
+ export function DocumentEditorModal({
16
+ isNew,
17
+ editingPath,
18
+ newPath,
19
+ editContent,
20
+ saving,
21
+ onPathChange,
22
+ onContentChange,
23
+ onSave,
24
+ onCancel
25
+ }: DocumentEditorModalProps) {
26
+ return (
27
+ <div style={{ position: 'fixed', top: 0, left: 0, right: 0, bottom: 0, background: 'rgba(0,0,0,0.8)', display: 'flex', alignItems: 'center', justifyContent: 'center', zIndex: 1000 }}>
28
+ <div style={{ background: '#1e1e1e', width: '90%', height: '90%', borderRadius: '8px', display: 'flex', flexDirection: 'column', border: '1px solid rgba(255,255,255,0.1)' }}>
29
+ <div style={{ padding: '20px', borderBottom: '1px solid rgba(255,255,255,0.1)', display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
30
+ <h2 style={{ margin: 0 }}>{isNew ? 'New Document' : `Edit ${editingPath}`}</h2>
31
+ <div>
32
+ <button onClick={onCancel} style={{ marginRight: '10px', padding: '8px 16px', background: 'rgba(255,255,255,0.1)', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>Cancel</button>
33
+ <button onClick={onSave} disabled={saving} style={{ padding: '8px 16px', background: '#4a90e2', color: 'white', border: 'none', borderRadius: '4px', cursor: 'pointer' }}>{saving ? 'Saving...' : 'Save'}</button>
34
+ </div>
35
+ </div>
36
+ <div style={{ padding: '20px', borderBottom: '1px solid rgba(255,255,255,0.1)' }}>
37
+ <label style={{ display: 'block', marginBottom: '5px' }}>File Path</label>
38
+ <input
39
+ type="text"
40
+ value={newPath}
41
+ onChange={e => onPathChange(e.target.value)}
42
+ disabled={!isNew}
43
+ style={{ width: '100%', padding: '8px', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', color: 'white', fontFamily: 'monospace' }}
44
+ />
45
+ </div>
46
+ <div style={{ flex: 1, padding: '20px', display: 'flex' }}>
47
+ <textarea
48
+ value={editContent}
49
+ onChange={e => onContentChange(e.target.value)}
50
+ style={{ width: '100%', height: '100%', background: 'rgba(0,0,0,0.3)', border: '1px solid rgba(255,255,255,0.1)', color: 'white', fontFamily: 'monospace', padding: '10px', resize: 'none' }}
51
+ placeholder="---&#10;type: memory&#10;title: New Document&#10;---&#10;&#10;Content goes here..."
52
+ />
53
+ </div>
54
+ </div>
55
+ </div>
56
+ );
57
+ }
@@ -0,0 +1,47 @@
1
+ import type { ReactNode } from 'react';
2
+
3
+ export interface Column<T> {
4
+ key: string;
5
+ header: string;
6
+ render?: (row: T) => ReactNode;
7
+ }
8
+
9
+ export interface DocumentTableProps<T> {
10
+ data: T[];
11
+ columns: Column<T>[];
12
+ emptyMessage?: string;
13
+ }
14
+
15
+ export function DocumentTable<T extends Record<string, unknown>>({ data, columns, emptyMessage = "No documents found." }: DocumentTableProps<T>) {
16
+ return (
17
+ <table style={{ width: '100%', borderCollapse: 'collapse', textAlign: 'left', marginTop: '10px' }}>
18
+ <thead>
19
+ <tr style={{ borderBottom: '1px solid rgba(255,255,255,0.1)' }}>
20
+ {columns.map((col, idx) => (
21
+ <th key={col.key} style={{ padding: '10px 0', textAlign: idx === columns.length - 1 && col.header === 'Actions' ? 'right' : 'left' }}>
22
+ {col.header}
23
+ </th>
24
+ ))}
25
+ </tr>
26
+ </thead>
27
+ <tbody>
28
+ {data.map((row, i) => (
29
+ <tr key={String(row.id || row.path || i)} style={{ borderBottom: '1px solid rgba(255,255,255,0.05)' }}>
30
+ {columns.map((col, idx) => (
31
+ <td key={col.key} style={{ padding: '10px 0', textAlign: idx === columns.length - 1 && col.header === 'Actions' ? 'right' : 'left' }}>
32
+ {col.render ? col.render(row) : String(row[col.key] || '-')}
33
+ </td>
34
+ ))}
35
+ </tr>
36
+ ))}
37
+ {data.length === 0 && (
38
+ <tr>
39
+ <td colSpan={columns.length} style={{ padding: '20px', textAlign: 'center', opacity: 0.5 }}>
40
+ {emptyMessage}
41
+ </td>
42
+ </tr>
43
+ )}
44
+ </tbody>
45
+ </table>
46
+ );
47
+ }