ltcai 4.0.0 → 4.1.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 (195) hide show
  1. package/README.md +42 -33
  2. package/desktop/electron/main.cjs +44 -0
  3. package/docs/CHANGELOG.md +106 -0
  4. package/docs/REALTIME_COLLABORATION.md +3 -3
  5. package/docs/V3_FRONTEND.md +9 -8
  6. package/docs/V4_1_FRONTEND_ARCHITECTURE_REVIEW.md +65 -0
  7. package/docs/V4_1_FRONTEND_MIGRATION_REPORT.md +70 -0
  8. package/docs/V4_1_VALIDATION_REPORT.md +47 -0
  9. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +95 -45
  10. package/docs/kg-schema.md +6 -2
  11. package/docs/spec-vs-impl.md +10 -10
  12. package/frontend/index.html +24 -0
  13. package/frontend/openapi.json +14190 -0
  14. package/frontend/src/App.tsx +184 -0
  15. package/frontend/src/api/client.ts +317 -0
  16. package/frontend/src/api/openapi.ts +16637 -0
  17. package/frontend/src/components/primitives.tsx +204 -0
  18. package/frontend/src/components/ui/badge.tsx +27 -0
  19. package/frontend/src/components/ui/button.tsx +37 -0
  20. package/frontend/src/components/ui/card.tsx +22 -0
  21. package/frontend/src/components/ui/input.tsx +16 -0
  22. package/frontend/src/components/ui/textarea.tsx +16 -0
  23. package/frontend/src/lib/utils.ts +33 -0
  24. package/frontend/src/main.tsx +23 -0
  25. package/frontend/src/pages/Act.tsx +245 -0
  26. package/frontend/src/pages/Ask.tsx +200 -0
  27. package/frontend/src/pages/Brain.tsx +267 -0
  28. package/frontend/src/pages/Capture.tsx +158 -0
  29. package/frontend/src/pages/Library.tsx +187 -0
  30. package/frontend/src/pages/System.tsx +344 -0
  31. package/frontend/src/routes.ts +85 -0
  32. package/frontend/src/store/appStore.ts +54 -0
  33. package/frontend/src/styles.css +107 -0
  34. package/kg_schema.py +2 -603
  35. package/knowledge_graph.py +37 -4958
  36. package/latticeai/__init__.py +1 -1
  37. package/latticeai/api/admin.py +15 -16
  38. package/latticeai/api/agents.py +13 -6
  39. package/latticeai/api/auth.py +19 -11
  40. package/latticeai/api/invitations.py +100 -0
  41. package/latticeai/api/knowledge_graph.py +4 -11
  42. package/latticeai/api/plugins.py +3 -6
  43. package/latticeai/api/realtime.py +4 -7
  44. package/latticeai/api/setup.py +5 -4
  45. package/latticeai/api/static_routes.py +13 -16
  46. package/latticeai/api/ui_redirects.py +26 -0
  47. package/latticeai/api/workflow_designer.py +39 -6
  48. package/latticeai/api/workspace.py +24 -10
  49. package/latticeai/app_factory.py +88 -17
  50. package/latticeai/brain/_kg_common.py +1123 -0
  51. package/latticeai/brain/discovery.py +1455 -0
  52. package/latticeai/brain/documents.py +218 -0
  53. package/latticeai/brain/ingest.py +644 -0
  54. package/latticeai/brain/projection.py +561 -0
  55. package/latticeai/brain/provenance.py +401 -0
  56. package/latticeai/brain/retrieval.py +1316 -0
  57. package/latticeai/brain/schema.py +640 -0
  58. package/latticeai/brain/store.py +216 -0
  59. package/latticeai/brain/write_master.py +225 -0
  60. package/latticeai/core/invitations.py +131 -0
  61. package/latticeai/core/marketplace.py +1 -1
  62. package/latticeai/core/multi_agent.py +1 -1
  63. package/latticeai/core/policy.py +54 -0
  64. package/latticeai/core/realtime.py +65 -44
  65. package/latticeai/core/sessions.py +31 -5
  66. package/latticeai/core/users.py +147 -0
  67. package/latticeai/core/workspace_os.py +420 -20
  68. package/latticeai/services/agent_runtime.py +242 -4
  69. package/latticeai/services/run_executor.py +328 -0
  70. package/latticeai/services/workspace_service.py +27 -19
  71. package/package.json +54 -27
  72. package/scripts/build_frontend_assets.mjs +38 -0
  73. package/scripts/bump_version.py +1 -1
  74. package/scripts/export_openapi.py +31 -0
  75. package/scripts/lint_frontend.mjs +86 -0
  76. package/scripts/run_python.mjs +47 -0
  77. package/src-tauri/Cargo.lock +4833 -0
  78. package/src-tauri/Cargo.toml +19 -0
  79. package/src-tauri/build.rs +3 -0
  80. package/src-tauri/capabilities/default.json +7 -0
  81. package/src-tauri/src/main.rs +78 -0
  82. package/src-tauri/tauri.conf.json +36 -0
  83. package/static/app/asset-manifest.json +32 -0
  84. package/static/app/assets/core-CwxXejkd.js +2 -0
  85. package/static/app/assets/core-CwxXejkd.js.map +1 -0
  86. package/static/app/assets/index-CJRAzNnf.js +333 -0
  87. package/static/app/assets/index-CJRAzNnf.js.map +1 -0
  88. package/static/app/assets/index-CSwBBgf4.css +2 -0
  89. package/static/app/index.html +25 -0
  90. package/static/manifest.json +2 -2
  91. package/static/sw.js +4 -4
  92. package/scripts/build_v3_assets.mjs +0 -170
  93. package/scripts/lint_v3.mjs +0 -97
  94. package/static/account.html +0 -113
  95. package/static/activity.html +0 -73
  96. package/static/admin.html +0 -486
  97. package/static/agents.html +0 -139
  98. package/static/chat.html +0 -841
  99. package/static/css/reference/account.css +0 -439
  100. package/static/css/reference/admin.css +0 -610
  101. package/static/css/reference/base.css +0 -1661
  102. package/static/css/reference/chat.css +0 -4623
  103. package/static/css/reference/graph.css +0 -1016
  104. package/static/css/responsive.css +0 -861
  105. package/static/graph.html +0 -122
  106. package/static/platform.css +0 -104
  107. package/static/plugins.html +0 -136
  108. package/static/scripts/account.js +0 -238
  109. package/static/scripts/admin.js +0 -1614
  110. package/static/scripts/chat.js +0 -5081
  111. package/static/scripts/graph.js +0 -1804
  112. package/static/scripts/platform.js +0 -64
  113. package/static/scripts/ux.js +0 -167
  114. package/static/scripts/workspace.js +0 -948
  115. package/static/v3/asset-manifest.json +0 -56
  116. package/static/v3/css/lattice.base.49deefb5.css +0 -128
  117. package/static/v3/css/lattice.base.css +0 -128
  118. package/static/v3/css/lattice.components.cde18231.css +0 -472
  119. package/static/v3/css/lattice.components.css +0 -472
  120. package/static/v3/css/lattice.shell.29d36d85.css +0 -452
  121. package/static/v3/css/lattice.shell.css +0 -452
  122. package/static/v3/css/lattice.tokens.304cbc40.css +0 -135
  123. package/static/v3/css/lattice.tokens.css +0 -135
  124. package/static/v3/css/lattice.views.0a18b6c5.css +0 -360
  125. package/static/v3/css/lattice.views.css +0 -360
  126. package/static/v3/index.html +0 -68
  127. package/static/v3/js/app.356e6452.js +0 -26
  128. package/static/v3/js/app.js +0 -26
  129. package/static/v3/js/core/api.7a308b89.js +0 -568
  130. package/static/v3/js/core/api.js +0 -568
  131. package/static/v3/js/core/components.f25b3b93.js +0 -230
  132. package/static/v3/js/core/components.js +0 -230
  133. package/static/v3/js/core/dom.a2773eb0.js +0 -148
  134. package/static/v3/js/core/dom.js +0 -148
  135. package/static/v3/js/core/router.584570f2.js +0 -37
  136. package/static/v3/js/core/router.js +0 -37
  137. package/static/v3/js/core/routes.7222343d.js +0 -93
  138. package/static/v3/js/core/routes.js +0 -93
  139. package/static/v3/js/core/shell.a1657f20.js +0 -391
  140. package/static/v3/js/core/shell.js +0 -391
  141. package/static/v3/js/core/store.204a08b2.js +0 -113
  142. package/static/v3/js/core/store.js +0 -113
  143. package/static/v3/js/views/admin-audit.660a1fb1.js +0 -185
  144. package/static/v3/js/views/admin-audit.js +0 -185
  145. package/static/v3/js/views/admin-permissions.a7ae5f09.js +0 -177
  146. package/static/v3/js/views/admin-permissions.js +0 -177
  147. package/static/v3/js/views/admin-policies.3658fd86.js +0 -102
  148. package/static/v3/js/views/admin-policies.js +0 -102
  149. package/static/v3/js/views/admin-private-vpc.7d342d36.js +0 -135
  150. package/static/v3/js/views/admin-private-vpc.js +0 -135
  151. package/static/v3/js/views/admin-security.07c66b72.js +0 -180
  152. package/static/v3/js/views/admin-security.js +0 -180
  153. package/static/v3/js/views/admin-users.03bac88c.js +0 -168
  154. package/static/v3/js/views/admin-users.js +0 -168
  155. package/static/v3/js/views/agents.014d0b74.js +0 -541
  156. package/static/v3/js/views/agents.js +0 -541
  157. package/static/v3/js/views/chat.e6dd7dd0.js +0 -601
  158. package/static/v3/js/views/chat.js +0 -601
  159. package/static/v3/js/views/files.adad14c1.js +0 -365
  160. package/static/v3/js/views/files.js +0 -365
  161. package/static/v3/js/views/graph-canvas.17c15d65.js +0 -509
  162. package/static/v3/js/views/graph-canvas.js +0 -509
  163. package/static/v3/js/views/home.24f8b8ae.js +0 -200
  164. package/static/v3/js/views/home.js +0 -200
  165. package/static/v3/js/views/hooks.37895880.js +0 -220
  166. package/static/v3/js/views/hooks.js +0 -220
  167. package/static/v3/js/views/hybrid-search.2fb63ed9.js +0 -194
  168. package/static/v3/js/views/hybrid-search.js +0 -194
  169. package/static/v3/js/views/knowledge-graph.5e40cbeb.js +0 -509
  170. package/static/v3/js/views/knowledge-graph.js +0 -509
  171. package/static/v3/js/views/marketplace.ab0583d4.js +0 -141
  172. package/static/v3/js/views/marketplace.js +0 -141
  173. package/static/v3/js/views/mcp.99b5c6a7.js +0 -114
  174. package/static/v3/js/views/mcp.js +0 -114
  175. package/static/v3/js/views/memory.4ebdf474.js +0 -147
  176. package/static/v3/js/views/memory.js +0 -147
  177. package/static/v3/js/views/models.a1ffa147.js +0 -256
  178. package/static/v3/js/views/models.js +0 -256
  179. package/static/v3/js/views/my-computer.d9d9ae1c.js +0 -463
  180. package/static/v3/js/views/my-computer.js +0 -463
  181. package/static/v3/js/views/pipeline.c522f1ce.js +0 -157
  182. package/static/v3/js/views/pipeline.js +0 -157
  183. package/static/v3/js/views/planning.9ac3e313.js +0 -153
  184. package/static/v3/js/views/planning.js +0 -153
  185. package/static/v3/js/views/settings.8631fa5e.js +0 -318
  186. package/static/v3/js/views/settings.js +0 -318
  187. package/static/v3/js/views/skills.c6c2f965.js +0 -109
  188. package/static/v3/js/views/skills.js +0 -109
  189. package/static/v3/js/views/tools.e4f11276.js +0 -108
  190. package/static/v3/js/views/tools.js +0 -108
  191. package/static/v3/js/views/workflows.26c57290.js +0 -128
  192. package/static/v3/js/views/workflows.js +0 -128
  193. package/static/workflows.html +0 -146
  194. package/static/workspace.css +0 -1121
  195. package/static/workspace.html +0 -357
@@ -1,948 +0,0 @@
1
- const API_BASE = window.location.protocol === "file:" ? "http://localhost:4825" : "";
2
-
3
- const state = {
4
- os: null,
5
- snapshots: [],
6
- activeWorkspace: null,
7
- registry: null,
8
- managingWorkspace: null,
9
- skillsPayload: null,
10
- skillTab: "recommended",
11
- skillProgress: {},
12
- entities: [],
13
- activeEntity: null,
14
- };
15
-
16
- // Skills that match common workspace needs are surfaced under "Recommended".
17
- const RECOMMENDED_SKILL_HINTS = ["code", "review", "doc", "test", "security", "research", "changelog", "refactor", "debug"];
18
- const MODE_KEY = "ltcai_workspace_mode";
19
- const LANG_KEY = "ltcai_lang";
20
-
21
- function $(id) {
22
- return document.getElementById(id);
23
- }
24
-
25
- function escapeHtml(value) {
26
- return String(value ?? "")
27
- .replaceAll("&", "&")
28
- .replaceAll("<", "&lt;")
29
- .replaceAll(">", "&gt;")
30
- .replaceAll('"', "&quot;")
31
- .replaceAll("'", "&#039;");
32
- }
33
-
34
- async function api(path, options = {}) {
35
- const headers = { ...(options.headers || {}) };
36
- if (options.body && !headers["Content-Type"]) headers["Content-Type"] = "application/json";
37
- if (state.activeWorkspace && !headers["X-Workspace-Id"]) headers["X-Workspace-Id"] = state.activeWorkspace;
38
- const response = await fetch(API_BASE + path, { credentials: "include", ...options, headers });
39
- const text = await response.text();
40
- let data = {};
41
- try { data = text ? JSON.parse(text) : {}; } catch { data = { detail: text }; }
42
- if (!response.ok) {
43
- throw new Error(data.detail || `${response.status} ${response.statusText}`);
44
- }
45
- return data;
46
- }
47
-
48
- function toast(message) {
49
- const node = $("toast");
50
- node.textContent = message;
51
- node.classList.add("show");
52
- clearTimeout(node._timer);
53
- node._timer = setTimeout(() => node.classList.remove("show"), 2200);
54
- }
55
-
56
- function adminAvailableForWorkspace(workspace) {
57
- const storedAdmin = (() => {
58
- try { return localStorage.getItem("ltcai_is_admin") === "true"; } catch { return false; }
59
- })();
60
- const role = String(workspace?.your_role || "").toLowerCase();
61
- return storedAdmin || role === "owner" || role === "admin";
62
- }
63
-
64
- function currentWorkspaceMode() {
65
- try {
66
- const mode = localStorage.getItem(MODE_KEY);
67
- return ["basic", "advanced", "admin"].includes(mode) ? mode : "basic";
68
- } catch {
69
- return "basic";
70
- }
71
- }
72
-
73
- function applyWorkspaceMode(mode, { adminAvailable = false } = {}) {
74
- if (mode === "admin" && !adminAvailable) mode = "basic";
75
- const shell = document.querySelector(".workspace-shell");
76
- if (shell) shell.dataset.workspaceMode = mode;
77
- document.querySelectorAll("[data-workspace-mode]").forEach((button) => {
78
- if (button.matches("button")) {
79
- const active = button.dataset.workspaceMode === mode;
80
- button.classList.toggle("active", active);
81
- button.setAttribute("aria-selected", active ? "true" : "false");
82
- button.disabled = button.dataset.workspaceMode === "admin" && !adminAvailable;
83
- }
84
- });
85
- try { localStorage.setItem(MODE_KEY, mode); } catch {}
86
- }
87
-
88
- function updateWorkspaceChrome(activeWorkspace) {
89
- const shell = document.querySelector(".workspace-shell");
90
- const adminAvailable = adminAvailableForWorkspace(activeWorkspace);
91
- if (shell) shell.dataset.adminAvailable = adminAvailable ? "true" : "false";
92
- applyWorkspaceMode(currentWorkspaceMode(), { adminAvailable });
93
- const lang = (() => {
94
- try { return localStorage.getItem(LANG_KEY) || "en"; } catch { return "en"; }
95
- })();
96
- document.documentElement.lang = lang;
97
- const langSelect = $("workspace-language");
98
- if (langSelect) langSelect.value = lang;
99
- }
100
-
101
- async function logoutWorkspace() {
102
- try { await api("/logout", { method: "POST" }); } catch (_) {}
103
- try {
104
- localStorage.removeItem("ltcai_user_email");
105
- localStorage.removeItem("ltcai_user_nickname");
106
- localStorage.removeItem("ltcai_is_admin");
107
- } catch (_) {}
108
- window.location.href = "/account";
109
- }
110
-
111
- function renderMetrics(os) {
112
- const counts = os?.counts || {};
113
- const graph = os?.graph || {};
114
- const nodeTotal = Object.values(graph.nodes || {}).reduce((sum, value) => sum + Number(value || 0), 0);
115
- const edgeTotal = Object.values(graph.edges || {}).reduce((sum, value) => sum + Number(value || 0), 0);
116
- const items = [
117
- ["Graph Nodes", nodeTotal, "ti-chart-dots-3"],
118
- ["Graph Edges", edgeTotal, "ti-git-branch"],
119
- ["Snapshots", counts.snapshots || 0, "ti-stack-2"],
120
- ["Memories", counts.memories || 0, "ti-book-2"],
121
- ];
122
- $("metric-grid").innerHTML = items.map(([label, value, icon]) => `
123
- <div class="metric-card">
124
- <i class="ti ${icon}"></i>
125
- <span>${escapeHtml(label)}</span>
126
- <strong>${escapeHtml(value)}</strong>
127
- </div>
128
- `).join("");
129
- }
130
-
131
- function latestTimestamp(...groups) {
132
- const values = groups.flat().filter(Boolean).map((value) => {
133
- const stamp = new Date(value);
134
- return Number.isNaN(stamp.getTime()) ? null : stamp;
135
- }).filter(Boolean);
136
- if (!values.length) return "";
137
- return new Date(Math.max(...values.map((stamp) => stamp.getTime()))).toISOString().slice(0, 19).replace("T", " ");
138
- }
139
-
140
- function renderWorkspaceHealth({ os, indexing, skills, timeline }) {
141
- const counts = os?.counts || {};
142
- const graph = os?.graph || {};
143
- const nodes = Object.values(graph.nodes || {}).reduce((sum, value) => sum + Number(value || 0), 0);
144
- const edges = Object.values(graph.edges || {}).reduce((sum, value) => sum + Number(value || 0), 0);
145
- const sources = indexing?.sources || [];
146
- const indexedFiles = sources.reduce((sum, source) => {
147
- const fileStatus = source.file_status || {};
148
- return sum + Number(fileStatus.indexed ?? source.success_count ?? 0);
149
- }, 0);
150
- const sourceTimes = sources.flatMap((source) => [source.last_run_at, source.last_scanned_at, source.updated_at]);
151
- const eventTimes = (timeline?.events || []).slice(0, 10).map((event) => event.timestamp);
152
- const currentModel = os?.models?.current_model || os?.models?.local_model || os?.models?.public_model || "not loaded";
153
- const status = nodes || indexedFiles || counts.memories || counts.agent_runs ? "ready" : "empty";
154
- const statusEl = $("workspace-health-status");
155
- if (statusEl) {
156
- statusEl.textContent = status;
157
- statusEl.className = `status-pill ${status === "ready" ? "status-complete" : "status-running"}`;
158
- }
159
- const items = [
160
- ["Indexed Files", indexedFiles, "ti-files", sources.length ? `${sources.length} source(s)` : "No indexed sources"],
161
- ["Graph Nodes", nodes, "ti-chart-dots-3", `${edges.toLocaleString()} relationship(s)`],
162
- ["Graph Relationships", edges, "ti-git-branch", "Knowledge links"],
163
- ["Installed Skills", skills?.total_installed ?? counts.skills ?? 0, "ti-puzzle", `${skills?.total_available ?? 0} available`],
164
- ["Memory Entries", counts.memories || 0, "ti-book-2", "Workspace memory"],
165
- ["Agent Runs", counts.agent_runs || 0, "ti-route-alt-left", `${counts.workflows || 0} workflow(s)`],
166
- ["Current Model", currentModel, "ti-cpu", `${(os?.models?.loaded_models || []).length} loaded`],
167
- ["Last Sync Time", latestTimestamp(os?.updated_at, sourceTimes, eventTimes) || "not synced", "ti-clock", `v${os?.version || "unknown"}`],
168
- ];
169
- const grid = $("workspace-health-grid");
170
- if (!grid) return;
171
- grid.innerHTML = items.map(([label, value, icon, meta]) => `
172
- <div class="health-card">
173
- <i class="ti ${icon}"></i>
174
- <span>${escapeHtml(label)}</span>
175
- <strong>${escapeHtml(value)}</strong>
176
- <em>${escapeHtml(meta)}</em>
177
- </div>
178
- `).join("");
179
- }
180
-
181
- function renderOnboarding(payload) {
182
- const steps = payload.steps || [];
183
- $("onboarding-steps").innerHTML = steps.map((step) => {
184
- const status = step.status || "pending";
185
- const label = step.id.replaceAll("_", " ");
186
- return `
187
- <button class="step-chip" data-step="${escapeHtml(step.id)}" title="Mark ${escapeHtml(label)} complete">
188
- <span>${escapeHtml(label)}</span>
189
- <span class="status-pill status-${escapeHtml(status)}">${escapeHtml(status)}</span>
190
- </button>
191
- `;
192
- }).join("");
193
- }
194
-
195
- function renderTraces(payload) {
196
- const traces = payload.traces || [];
197
- $("trace-list").innerHTML = traces.length ? traces.map((trace) => `
198
- <div class="list-item">
199
- <div class="list-title">
200
- <span>${escapeHtml(trace.question || "Trace")}</span>
201
- <span class="status-pill">${Math.round((trace.confidence || 0) * 100)}%</span>
202
- </div>
203
- <div class="meta-line">${escapeHtml(trace.created_at || "")} · ${escapeHtml(trace.conversation_id || "workspace")}</div>
204
- <div class="tag-row">
205
- ${(trace.graph_nodes || []).slice(0, 5).map((node) => `<a class="tag" href="/graph?node=${encodeURIComponent(node.id)}">${escapeHtml(node.title || node.id)}</a>`).join("")}
206
- </div>
207
- <div class="mini-row">${escapeHtml((trace.source_files || []).map((source) => source.source).slice(0, 3).join(" · ") || "No source files")}</div>
208
- </div>
209
- `).join("") : `<div class="list-item"><div class="meta-line">No answer traces yet.</div></div>`;
210
- }
211
-
212
- function renderIndexing(payload) {
213
- const sources = payload.sources || [];
214
- $("indexing-list").innerHTML = sources.length ? sources.map((source) => `
215
- <div class="list-item">
216
- <div class="list-title">
217
- <span>${escapeHtml(source.label || source.root_path)}</span>
218
- <span class="status-pill ${source.watch_active ? "status-complete" : ""}">${source.watch_active ? "watching" : source.status || "idle"}</span>
219
- </div>
220
- <div class="meta-line">${escapeHtml(source.root_path || "")}</div>
221
- <div class="tag-row">
222
- <span class="tag">${Number(source.success_count || 0)} indexed</span>
223
- <span class="tag">${Number(source.failure_count || 0)} failed</span>
224
- <span class="tag">${escapeHtml(source.last_run_at || "not scanned")}</span>
225
- </div>
226
- <div class="item-actions">
227
- <button class="small-action" data-index-action="resume" data-source="${escapeHtml(source.id)}"><i class="ti ti-player-play"></i>Resume</button>
228
- <button class="small-action" data-index-action="pause" data-source="${escapeHtml(source.id)}"><i class="ti ti-player-pause"></i>Pause</button>
229
- <button class="small-action danger-action" data-index-action="remove" data-source="${escapeHtml(source.id)}"><i class="ti ti-trash"></i>Remove</button>
230
- </div>
231
- </div>
232
- `).join("") : `<div class="list-item"><div class="meta-line">No indexed folders.</div></div>`;
233
- }
234
-
235
- function renderSnapshots(payload) {
236
- const snapshots = payload.snapshots || [];
237
- state.snapshots = snapshots;
238
- $("snapshot-list").innerHTML = snapshots.length ? snapshots.map((snapshot) => `
239
- <div class="list-item">
240
- <div class="list-title">
241
- <span>${escapeHtml(snapshot.name)}</span>
242
- <span class="status-pill">${escapeHtml(snapshot.node_count || 0)} nodes</span>
243
- </div>
244
- <div class="meta-line">${escapeHtml(snapshot.created_at)} · ${escapeHtml(snapshot.id)}</div>
245
- <div class="item-actions">
246
- <button class="small-action" data-export-snapshot="${escapeHtml(snapshot.id)}"><i class="ti ti-package-export"></i>Export</button>
247
- </div>
248
- </div>
249
- `).join("") : `<div class="list-item"><div class="meta-line">No snapshots.</div></div>`;
250
-
251
- const options = snapshots.map((snapshot) => `<option value="${escapeHtml(snapshot.id)}">${escapeHtml(snapshot.name)}</option>`).join("");
252
- $("snapshot-before").innerHTML = options;
253
- $("snapshot-after").innerHTML = options;
254
- if (snapshots[1]) $("snapshot-before").value = snapshots[1].id;
255
- if (snapshots[0]) $("snapshot-after").value = snapshots[0].id;
256
- }
257
-
258
- function renderMemories(payload) {
259
- const memories = payload.memories || [];
260
- $("memory-list").innerHTML = memories.length ? memories.map((memory) => `
261
- <div class="list-item">
262
- <div class="list-title">
263
- <span>${escapeHtml(memory.kind || "memory")}</span>
264
- <span class="status-pill">${escapeHtml(memory.updated_at || "")}</span>
265
- </div>
266
- <div>${escapeHtml(memory.content || "")}</div>
267
- <div class="tag-row">${(memory.tags || []).map((tag) => `<span class="tag">${escapeHtml(tag)}</span>`).join("")}</div>
268
- </div>
269
- `).join("") : `<div class="list-item"><div class="meta-line">No personal memory yet.</div></div>`;
270
- }
271
-
272
- function renderComputerMemory(payload) {
273
- const config = payload?.computer_memory || payload || {};
274
- $("computer-memory-toggle").checked = Boolean(config.enabled);
275
- $("computer-memory-state").textContent = JSON.stringify({
276
- enabled: Boolean(config.enabled),
277
- approved: Boolean(config.approved),
278
- scopes: config.scopes || [],
279
- activities: (config.activities || []).length,
280
- notice: config.notice,
281
- }, null, 2);
282
- }
283
-
284
- function renderAgents(payload) {
285
- const agents = payload.agents || [];
286
- $("agent-list").innerHTML = agents.map((agent) => `
287
- <div class="list-item">
288
- <div class="list-title">
289
- <span>${escapeHtml(agent.name)}</span>
290
- <span class="status-pill status-complete">${escapeHtml(agent.status || "available")}</span>
291
- </div>
292
- <div class="meta-line">${escapeHtml(agent.role || "")}</div>
293
- <div class="tag-row">${(agent.relationships || []).map((rel) => `<span class="tag">${escapeHtml(rel)}</span>`).join("")}</div>
294
- </div>
295
- `).join("");
296
- }
297
-
298
- function renderWorkflows(payload) {
299
- const workflows = payload.workflows || [];
300
- $("workflow-list").innerHTML = workflows.length ? workflows.map((workflow) => `
301
- <div class="list-item">
302
- <div class="list-title">
303
- <span>${escapeHtml(workflow.name)}</span>
304
- <span class="status-pill">${(workflow.steps || []).length} steps</span>
305
- </div>
306
- <div class="meta-line">${escapeHtml(workflow.created_at || "")}</div>
307
- <div class="tag-row">${(workflow.steps || []).slice(0, 4).map((step) => `<span class="tag">${escapeHtml(step.action || step.name || "step")}</span>`).join("")}</div>
308
- </div>
309
- `).join("") : `<div class="list-item"><div class="meta-line">No workflows.</div></div>`;
310
- }
311
-
312
- function skillName(skill) {
313
- return skill.skill || skill.name || "skill";
314
- }
315
-
316
- function skillProgress(name) {
317
- return state.skillProgress[name] || null;
318
- }
319
-
320
- // Compute the four marketplace tabs from the registry payload (machine-global
321
- // registry + locally-installed state). "Updates" = installed skills whose
322
- // registry version differs from the installed version.
323
- function computeSkillTabs(payload) {
324
- const installed = payload.installed || [];
325
- const available = payload.available || [];
326
- const installedNames = new Set(installed.map(skillName));
327
- const notInstalled = available.filter((s) => !installedNames.has(skillName(s)));
328
- const availByName = new Map(available.map((s) => [skillName(s), s]));
329
- const updates = installed.filter((s) => {
330
- const remote = availByName.get(skillName(s));
331
- return remote && remote.version && s.version && remote.version !== s.version;
332
- });
333
- const recommended = notInstalled.filter((s) => {
334
- const hay = `${skillName(s)} ${s.category || ""} ${s.description || ""}`.toLowerCase();
335
- return RECOMMENDED_SKILL_HINTS.some((h) => hay.includes(h));
336
- });
337
- const popular = notInstalled.slice().sort((a, b) => Number(b.downloads || b.popularity || 0) - Number(a.downloads || a.popularity || 0));
338
- return { installed, popular, recommended: recommended.length ? recommended : popular.slice(0, 8), updates };
339
- }
340
-
341
- function renderSkillRow(skill, { installed }) {
342
- const name = skillName(skill);
343
- const enabled = skill.enabled !== false;
344
- const version = skill.version || (installed ? "local" : "registry");
345
- const source = skill.plugin || skill.source || skill.source_url || (installed ? "installed" : "marketplace");
346
- const validation = skill.validation_status || (installed ? "ready" : "not installed");
347
- const installStatus = skill.install_status || (installed ? "ready" : "available");
348
- const progress = skillProgress(name);
349
- const actions = installed
350
- ? `<button class="small-action" data-skill-action="${enabled ? "disable" : "enable"}" data-skill="${escapeHtml(name)}"><i class="ti ti-${enabled ? "toggle-left" : "toggle-right"}"></i>${enabled ? "Disable" : "Enable"}</button>
351
- <button class="small-action" data-skill-action="update" data-skill="${escapeHtml(name)}"><i class="ti ti-refresh"></i>Update</button>`
352
- : `<button class="small-action" data-skill-action="install" data-skill="${escapeHtml(name)}" ${progress ? "disabled" : ""}><i class="ti ti-download"></i>Install</button>`;
353
- const progressHtml = progress ? `
354
- <div class="skill-progress" aria-label="Install progress">
355
- <div class="skill-progress-head"><span>${escapeHtml(progress.phase)}</span><span>${escapeHtml(progress.percent)}%</span></div>
356
- <div class="skill-progress-track"><span style="width:${Math.max(0, Math.min(100, progress.percent))}%"></span></div>
357
- </div>
358
- ` : "";
359
- return `
360
- <div class="list-item">
361
- <div class="list-title">
362
- <span>${escapeHtml(name)}</span>
363
- <span class="status-pill ${installed ? (enabled ? "status-complete" : "status-failed") : ""}">${installed ? (enabled ? "enabled" : "disabled") : "available"}</span>
364
- </div>
365
- <div class="meta-line">${escapeHtml(skill.description || "No description")}</div>
366
- <div class="tag-row">
367
- <span class="tag">v${escapeHtml(version)}</span>
368
- ${skill.category ? `<span class="tag">${escapeHtml(skill.category)}</span>` : ""}
369
- <span class="tag">${escapeHtml(source)}</span>
370
- <span class="tag">install: ${escapeHtml(installStatus)}</span>
371
- <span class="tag">validation: ${escapeHtml(validation)}</span>
372
- </div>
373
- ${progressHtml}
374
- <div class="item-actions">${actions}</div>
375
- </div>`;
376
- }
377
-
378
- function renderSkills(payload) {
379
- if (payload) state.skillsPayload = payload;
380
- const data = state.skillsPayload || { installed: [], available: [] };
381
- const tabs = computeSkillTabs(data);
382
- const updatesCount = $("skill-updates-count");
383
- if (updatesCount) updatesCount.textContent = tabs.updates.length ? String(tabs.updates.length) : "";
384
- document.querySelectorAll("[data-skill-tab]").forEach((btn) => {
385
- btn.classList.toggle("active", btn.dataset.skillTab === state.skillTab);
386
- });
387
- const tab = state.skillTab;
388
- const rows = (tab === "installed" || tab === "updates")
389
- ? (tabs[tab] || []).map((s) => renderSkillRow(s, { installed: true }))
390
- : (tabs[tab] || []).slice(0, 24).map((s) => renderSkillRow(s, { installed: false }));
391
- const empty = {
392
- recommended: "No recommended skills right now.",
393
- popular: "Marketplace is empty.",
394
- installed: "No skills installed yet.",
395
- updates: "All installed skills are up to date.",
396
- }[tab];
397
- $("skill-list").innerHTML = rows.length ? rows.join("") : `<div class="list-item"><div class="meta-line">${escapeHtml(empty)}</div></div>`;
398
- }
399
-
400
- function renderTimeline(payload) {
401
- const events = payload.events || [];
402
- $("timeline-list").innerHTML = events.length ? events.slice(0, 40).map((event) => `
403
- <div class="timeline-item">
404
- <div class="list-title"><span>${escapeHtml(event.event_type || "event")}</span><span class="status-pill">${escapeHtml(event.area || "workspace")}</span></div>
405
- <div class="meta-line">${escapeHtml(event.timestamp || "")}</div>
406
- </div>
407
- `).join("") : `<div class="timeline-item"><div class="meta-line">No timeline events.</div></div>`;
408
- }
409
-
410
- function renderWorkspaceRegistry(registry, edition) {
411
- state.registry = registry;
412
- if (!state.activeWorkspace) state.activeWorkspace = registry.active_workspace || "personal";
413
- const workspaces = registry.workspaces || [];
414
- const select = $("workspace-select");
415
- if (select) {
416
- select.innerHTML = workspaces.map((ws) => `
417
- <option value="${escapeHtml(ws.workspace_id)}" ${ws.workspace_id === state.activeWorkspace ? "selected" : ""}>
418
- ${escapeHtml(ws.name)}${ws.type === "organization" ? " (org)" : ""}${ws.status === "archived" ? " · archived" : ""}
419
- </option>
420
- `).join("");
421
- }
422
- const active = workspaces.find((ws) => ws.workspace_id === state.activeWorkspace);
423
- updateWorkspaceChrome(active);
424
- const rolePill = $("workspace-role");
425
- if (rolePill) rolePill.textContent = active ? (active.your_role || "—") : "";
426
- if (edition) {
427
- const pill = $("edition-pill");
428
- if (pill) pill.textContent = edition.edition || "community";
429
- }
430
- const list = $("workspace-list");
431
- if (list) {
432
- list.innerHTML = workspaces.length ? workspaces.map((ws) => `
433
- <div class="list-item">
434
- <div class="list-title">
435
- <span>${escapeHtml(ws.name)}</span>
436
- <span class="status-pill">${escapeHtml(ws.type)}</span>
437
- </div>
438
- <div class="meta-line">id: ${escapeHtml(ws.workspace_id)} · members: ${escapeHtml(ws.member_count)} · role: ${escapeHtml(ws.your_role || "—")} · ${escapeHtml(ws.status || "active")}</div>
439
- <div class="item-actions">
440
- ${ws.workspace_id === state.activeWorkspace ? `<span class="status-pill">active</span>` : `<button class="small-action" data-ws-action="activate" data-ws="${escapeHtml(ws.workspace_id)}"><i class="ti ti-switch-horizontal"></i>Switch</button>`}
441
- ${ws.type === "organization" ? `<button class="small-action" data-ws-action="members" data-ws="${escapeHtml(ws.workspace_id)}"><i class="ti ti-users"></i>Members</button>` : ""}
442
- ${ws.type === "organization" && ws.status !== "archived" ? `<button class="small-action" data-ws-action="archive" data-ws="${escapeHtml(ws.workspace_id)}"><i class="ti ti-archive"></i>Archive</button>` : ""}
443
- </div>
444
- </div>
445
- `).join("") : `<div class="list-item"><div class="meta-line">No workspaces.</div></div>`;
446
- }
447
- renderMembers(workspaces);
448
- }
449
-
450
- function renderMembers(workspaces) {
451
- const panel = $("member-panel");
452
- if (!panel) return;
453
- if (!state.managingWorkspace) {
454
- panel.hidden = true;
455
- return;
456
- }
457
- const ws = (workspaces || []).find((item) => item.workspace_id === state.managingWorkspace);
458
- if (!ws || ws.type !== "organization") {
459
- panel.hidden = true;
460
- return;
461
- }
462
- panel.hidden = false;
463
- const title = $("member-panel-title");
464
- if (title) title.textContent = `Members — ${ws.name}`;
465
- const members = ws.members || [];
466
- $("member-list").innerHTML = members.length ? members.map((m) => `
467
- <div class="list-item">
468
- <div class="list-title"><span>${escapeHtml(m.user_id)}</span><span class="status-pill">${escapeHtml(m.role)}</span></div>
469
- <div class="item-actions">
470
- <select class="small-action" data-member-role="${escapeHtml(m.user_id)}" data-ws="${escapeHtml(ws.workspace_id)}">
471
- ${["owner", "admin", "member", "viewer"].map((r) => `<option value="${r}" ${m.role === r ? "selected" : ""}>${r}</option>`).join("")}
472
- </select>
473
- <button class="small-action" data-member-remove="${escapeHtml(m.user_id)}" data-ws="${escapeHtml(ws.workspace_id)}"><i class="ti ti-user-minus"></i>Remove</button>
474
- </div>
475
- </div>
476
- `).join("") : `<div class="list-item"><div class="meta-line">No members yet.</div></div>`;
477
- }
478
-
479
- async function activateWorkspace(workspaceId) {
480
- await api("/workspace/activate", { method: "POST", body: JSON.stringify({ workspace_id: workspaceId }) });
481
- state.activeWorkspace = workspaceId;
482
- toast(`Switched to ${workspaceId}`);
483
- await refreshAll();
484
- }
485
-
486
- async function createOrg() {
487
- const name = ($("org-name").value || "").trim();
488
- if (!name) return;
489
- const result = await api("/workspace/orgs", { method: "POST", body: JSON.stringify({ name, settings: {} }) });
490
- $("org-name").value = "";
491
- toast(`Created ${result.workspace.workspace_id}`);
492
- state.managingWorkspace = result.workspace.workspace_id;
493
- await refreshAll();
494
- }
495
-
496
- async function addMember(workspaceId) {
497
- const userId = ($("member-user").value || "").trim();
498
- if (!userId) return;
499
- await api(`/workspace/orgs/${encodeURIComponent(workspaceId)}/members`, {
500
- method: "POST",
501
- body: JSON.stringify({ user_id: userId, role: $("member-role").value }),
502
- });
503
- $("member-user").value = "";
504
- toast(`Added ${userId}`);
505
- await refreshAll();
506
- }
507
-
508
- // ── Workspace summary (Phase 3) ──────────────────────────────────────────────
509
- function renderWorkspaceSummary(os) {
510
- const reg = os?.workspace_registry || {};
511
- const workspaces = reg.workspaces || [];
512
- const activeId = state.activeWorkspace || reg.active_workspace;
513
- const active = workspaces.find((w) => w.workspace_id === activeId) || workspaces[0] || { name: "Personal Workspace", type: "personal", your_role: "owner", member_count: 1 };
514
- const counts = os?.counts || {};
515
- const scopePill = $("summary-scope-pill");
516
- if (scopePill) scopePill.textContent = active.type || "personal";
517
- const summary = $("workspace-summary");
518
- if (summary) {
519
- const stats = [["Snapshots", counts.snapshots], ["Memories", counts.memories], ["Agent runs", counts.agent_runs], ["Workflows", counts.workflows], ["Traces", counts.traces], ["Timeline", counts.timeline]];
520
- summary.innerHTML = `
521
- <div class="summary-main">
522
- <div class="summary-icon"><i class="ti ${active.type === "organization" ? "ti-building-community" : "ti-user"}"></i></div>
523
- <div class="summary-id">
524
- <div class="summary-name">${escapeHtml(active.name || "Personal Workspace")}</div>
525
- <div class="meta-line">${escapeHtml(active.type || "personal")} workspace · your role <strong>${escapeHtml(active.your_role || "owner")}</strong> · ${escapeHtml(active.member_count ?? 1)} member(s)</div>
526
- </div>
527
- </div>
528
- <div class="summary-stats">
529
- ${stats.map(([l, v]) => `<div class="summary-stat"><strong>${escapeHtml(v || 0)}</strong><span>${escapeHtml(l)}</span></div>`).join("")}
530
- </div>`;
531
- }
532
- const quick = $("workspace-quickswitch");
533
- if (quick) {
534
- quick.innerHTML = workspaces.map((w) => `
535
- <button class="switch-chip ${w.workspace_id === activeId ? "active" : ""}" data-ws-action="activate" data-ws="${escapeHtml(w.workspace_id)}">
536
- <i class="ti ${w.type === "organization" ? "ti-building-community" : "ti-user"}"></i>
537
- <span>${escapeHtml(w.name)}</span>${w.workspace_id === activeId ? ' <i class="ti ti-check"></i>' : ""}
538
- </button>`).join("");
539
- }
540
- }
541
-
542
- // ── Knowledge Graph explorer (Phase 2) ───────────────────────────────────────
543
- const ENTITY_ICONS = { Person: "ti-user", Concept: "ti-bulb", Document: "ti-file-text", File: "ti-file", Code: "ti-code", Chat: "ti-message", Conversation: "ti-messages", Message: "ti-message-dots", Task: "ti-checklist", Decision: "ti-gavel", Error: "ti-alert-triangle", Model: "ti-cpu", Tool: "ti-tool", Project: "ti-folders", Feature: "ti-star", AIResponse: "ti-robot", Chunk: "ti-file-stack" };
544
- function entityIcon(type) { return ENTITY_ICONS[type] || "ti-point"; }
545
- function prettyId(id) { return String(id || "").split(":").slice(1).join(":") || String(id || ""); }
546
-
547
- async function loadGraphExplorer() {
548
- try {
549
- const data = await api("/knowledge-graph/graph?limit=150");
550
- const nodes = (data.nodes || []).slice();
551
- nodes.sort((a, b) => (b.importance ?? b.metadata?.graph_metrics?.importance_raw ?? 0) - (a.importance ?? a.metadata?.graph_metrics?.importance_raw ?? 0));
552
- state.entities = nodes;
553
- renderEntities();
554
- } catch (e) {
555
- const el = $("entity-list");
556
- if (el) el.innerHTML = `<div class="list-item"><div class="meta-line">Knowledge graph unavailable: ${escapeHtml(e.message)}</div></div>`;
557
- }
558
- }
559
-
560
- function renderEntities() {
561
- const el = $("entity-list");
562
- if (!el) return;
563
- const q = ($("entity-search")?.value || "").toLowerCase().trim();
564
- const filtered = q ? state.entities.filter((n) => `${n.title || ""} ${n.type || ""} ${n.id || ""}`.toLowerCase().includes(q)) : state.entities;
565
- const list = filtered.slice(0, 40);
566
- el.innerHTML = list.length ? list.map((n) => {
567
- const m = n.metadata?.graph_metrics || {};
568
- const imp = Math.round((n.importance_norm ?? m.importance_norm ?? 0) * 100);
569
- return `
570
- <button class="list-item entity-card ${n.id === state.activeEntity ? "selected" : ""}" data-entity="${escapeHtml(n.id)}">
571
- <div class="list-title"><span><i class="ti ${entityIcon(n.type)}"></i> ${escapeHtml(n.title || prettyId(n.id))}</span><span class="status-pill">${escapeHtml(n.type || "node")}</span></div>
572
- ${n.summary ? `<div class="meta-line">${escapeHtml(String(n.summary).slice(0, 110))}</div>` : ""}
573
- <div class="tag-row"><span class="tag">${escapeHtml(m.degree ?? 0)} links</span><span class="tag">importance ${imp}%</span></div>
574
- <div class="importance-bar"><span style="width:${imp}%"></span></div>
575
- </button>`;
576
- }).join("") : `<div class="list-item"><div class="meta-line">No matching entities.</div></div>`;
577
- }
578
-
579
- async function selectEntity(id) {
580
- state.activeEntity = id;
581
- renderEntities();
582
- const detail = $("entity-detail");
583
- const title = $("entity-detail-title");
584
- if (title) title.textContent = "Loading…";
585
- try {
586
- const d = await api(`/workspace/relationships/${encodeURIComponent(id)}`);
587
- const node = d.node || {};
588
- const related = d.related_entities || [];
589
- const relMap = new Map(related.map((r) => [r.id, r]));
590
- const labelFor = (nodeId) => { const r = relMap.get(nodeId); return r ? (r.title || prettyId(nodeId)) : prettyId(nodeId); };
591
- const edgeRow = (e, dir) => {
592
- const other = dir === "out" ? e.to : e.from;
593
- return `<div class="rel-row"><span class="rel-dir">${dir === "out" ? "→" : "←"}</span><span class="tag">${escapeHtml(e.type || "related")}</span><span class="rel-node">${escapeHtml(labelFor(other))}</span></div>`;
594
- };
595
- const inbound = (d.inbound || []).slice(0, 8);
596
- const outbound = (d.outbound || []).slice(0, 8);
597
- const path = Array.isArray(d.shortest_path) ? d.shortest_path : [];
598
- if (title) title.textContent = node.title || prettyId(id);
599
- detail.innerHTML = `
600
- <div class="list-item">
601
- <div class="list-title"><span><i class="ti ${entityIcon(node.type)}"></i> ${escapeHtml(node.title || prettyId(id))}</span><span class="status-pill">${escapeHtml(node.type || "node")}</span></div>
602
- ${node.summary ? `<div class="meta-line">${escapeHtml(node.summary)}</div>` : ""}
603
- <div class="tag-row"><span class="tag">importance ${Math.round((node.importance_norm || 0) * 100)}%</span><span class="tag">${inbound.length + outbound.length} relationships</span></div>
604
- </div>
605
- <div class="list-item"><div class="list-title"><span>Outbound</span><span class="status-pill">${outbound.length}</span></div>${outbound.map((e) => edgeRow(e, "out")).join("") || '<div class="meta-line">None</div>'}</div>
606
- <div class="list-item"><div class="list-title"><span>Inbound</span><span class="status-pill">${inbound.length}</span></div>${inbound.map((e) => edgeRow(e, "in")).join("") || '<div class="meta-line">None</div>'}</div>
607
- ${related.length ? `<div class="list-item"><div class="list-title"><span>Related entities</span><span class="status-pill">${related.length}</span></div><div class="tag-row">${related.slice(0, 10).map((r) => `<span class="tag"><i class="ti ${entityIcon(r.type)}"></i> ${escapeHtml(r.title || prettyId(r.id))}</span>`).join("")}</div></div>` : ""}
608
- ${path.length ? `<div class="list-item"><div class="list-title"><span>Path to you</span><span class="status-pill">${path.length} hops</span></div><div class="meta-line">${path.map((p) => escapeHtml(typeof p === "string" ? prettyId(p) : (p.title || prettyId(p.id)))).join(" → ")}</div></div>` : ""}
609
- <div class="item-actions"><a class="small-action" href="/graph?node=${encodeURIComponent(id)}"><i class="ti ti-network"></i>Open in Graph Canvas</a></div>`;
610
- } catch (e) {
611
- if (title) title.textContent = "Relationships";
612
- detail.innerHTML = `<div class="list-item"><div class="meta-line">No relationships available: ${escapeHtml(e.message)}</div></div>`;
613
- }
614
- }
615
-
616
- // ── Recent activity feed (Phase 2), built from already-fetched data ───────────
617
- function renderActivity({ traces, snapshots, memories, workflows, timeline }) {
618
- const items = [];
619
- (traces.traces || []).forEach((t) => items.push({ ts: t.created_at, icon: "ti-search", label: `Answer trace: ${t.question || "query"}`, tag: "graph rag" }));
620
- (snapshots.snapshots || []).forEach((s) => items.push({ ts: s.created_at, icon: "ti-stack-2", label: `Snapshot: ${s.name}`, tag: "snapshot" }));
621
- (memories.memories || []).forEach((m) => items.push({ ts: m.updated_at, icon: "ti-book-2", label: `Memory: ${(m.content || m.kind || "").slice(0, 60)}`, tag: m.kind || "memory" }));
622
- (workflows.workflows || []).forEach((w) => items.push({ ts: w.created_at, icon: "ti-git-branch", label: `Workflow: ${w.name}`, tag: "workflow" }));
623
- (timeline.events || []).forEach((e) => items.push({ ts: e.timestamp, icon: "ti-timeline-event", label: e.event_type || "event", tag: e.area || "workspace" }));
624
- items.sort((a, b) => String(b.ts || "").localeCompare(String(a.ts || "")));
625
- const el = $("activity-list");
626
- if (!el) return;
627
- el.innerHTML = items.length ? items.slice(0, 18).map((it) => `
628
- <div class="list-item activity-item">
629
- <div class="list-title"><span><i class="ti ${it.icon}"></i> ${escapeHtml(it.label)}</span><span class="status-pill">${escapeHtml(it.tag)}</span></div>
630
- <div class="meta-line">${escapeHtml(it.ts || "")}</div>
631
- </div>`).join("") : `<div class="list-item"><div class="meta-line">No recent activity yet — index a folder or ask a question to get started.</div></div>`;
632
- }
633
-
634
- function renderMemoryFeed(payload) {
635
- const memories = payload.memories || [];
636
- const el = $("memory-feed");
637
- if (!el) return;
638
- el.innerHTML = memories.length ? memories.slice(0, 8).map((m) => `
639
- <div class="list-item">
640
- <div class="list-title"><span><i class="ti ti-book-2"></i> ${escapeHtml(m.kind || "memory")}</span><span class="status-pill">${escapeHtml(m.updated_at || "")}</span></div>
641
- <div class="meta-line">${escapeHtml(String(m.content || "").slice(0, 140))}</div>
642
- </div>`).join("") : `<div class="list-item"><div class="meta-line">No workspace memory yet.</div></div>`;
643
- }
644
-
645
- // ── Enterprise capability panel (Phase 6) ─────────────────────────────────────
646
- const CAPABILITY_LABELS = {
647
- sso_advanced: "Advanced SSO", idp_provisioning: "IdP Provisioning", scim: "SCIM",
648
- rbac_abac_advanced: "Advanced RBAC/ABAC", tenant_isolation: "Tenant Isolation",
649
- compliance_retention: "Compliance Retention", siem_export: "SIEM Export",
650
- private_vpc: "Private VPC", air_gapped_deployment: "Air-gapped Deploy",
651
- dlp_policy: "DLP Policy", ediscovery: "eDiscovery", admin_policy_packs: "Admin Policy Packs",
652
- };
653
- function renderEnterprise(edition) {
654
- edition = edition || {};
655
- const caps = edition.capabilities || {};
656
- const editionName = edition.edition || "community";
657
- const pill = $("enterprise-edition-pill");
658
- if (pill) { pill.textContent = editionName; pill.className = `status-pill ${edition.is_enterprise ? "status-complete" : ""}`; }
659
- const note = $("enterprise-note");
660
- if (note) note.textContent = edition.community_notice || "Community edition: every Enterprise capability below is an extension point and is disabled. Nothing here gates a Community feature.";
661
- const grid = $("capability-grid");
662
- if (!grid) return;
663
- const keys = Object.keys(caps).length ? Object.keys(caps) : Object.keys(CAPABILITY_LABELS);
664
- grid.innerHTML = keys.map((k) => {
665
- const on = Boolean(caps[k]);
666
- return `
667
- <div class="capability-card ${on ? "on" : "off"}">
668
- <i class="ti ${on ? "ti-circle-check" : "ti-lock"}"></i>
669
- <span class="cap-name">${escapeHtml(CAPABILITY_LABELS[k] || k)}</span>
670
- <span class="status-pill ${on ? "status-complete" : "status-failed"}">${on ? "enabled" : "disabled"}</span>
671
- </div>`;
672
- }).join("");
673
- }
674
-
675
- async function refreshAll() {
676
- const [os, onboarding, traces, indexing, snapshots, memories, computerMemory, agents, workflows, skills, timeline] = await Promise.all([
677
- api("/workspace/os"),
678
- api("/workspace/onboarding/status"),
679
- api("/workspace/traces"),
680
- api("/workspace/indexing"),
681
- api("/workspace/snapshots"),
682
- api("/workspace/memories"),
683
- api("/workspace/computer-memory"),
684
- api("/workspace/agents"),
685
- api("/workspace/workflows"),
686
- api("/workspace/skills"),
687
- api("/workspace/time-machine"),
688
- ]);
689
- state.os = os;
690
- renderMetrics(os);
691
- renderWorkspaceHealth({ os, indexing, skills, timeline });
692
- if (os.workspace_registry) renderWorkspaceRegistry(os.workspace_registry, os.edition);
693
- renderOnboarding(onboarding);
694
- renderTraces(traces);
695
- renderIndexing(indexing);
696
- renderSnapshots(snapshots);
697
- renderMemories(memories);
698
- renderComputerMemory(computerMemory);
699
- renderAgents(agents);
700
- renderWorkflows(workflows);
701
- renderSkills(skills);
702
- renderTimeline(timeline);
703
- renderWorkspaceSummary(os);
704
- renderEnterprise(os.edition);
705
- renderActivity({ traces, snapshots, memories, workflows, timeline });
706
- renderMemoryFeed(memories);
707
- loadGraphExplorer();
708
- }
709
-
710
- async function createSnapshot() {
711
- const name = $("snapshot-name").value || "Workspace snapshot";
712
- const payload = await api("/workspace/snapshots", {
713
- method: "POST",
714
- body: JSON.stringify({ name }),
715
- });
716
- toast(`Snapshot saved: ${payload.snapshot.id}`);
717
- await refreshAll();
718
- }
719
-
720
- async function compareSnapshots() {
721
- const beforeId = $("snapshot-before").value;
722
- const afterId = $("snapshot-after").value;
723
- if (!beforeId || !afterId) return;
724
- const diff = await api("/workspace/snapshots/compare", {
725
- method: "POST",
726
- body: JSON.stringify({ before_id: beforeId, after_id: afterId }),
727
- });
728
- $("snapshot-diff").textContent = JSON.stringify(diff.summary, null, 2);
729
- }
730
-
731
- async function saveMemory() {
732
- const content = $("memory-content").value.trim();
733
- if (!content) return;
734
- await api("/workspace/memories", {
735
- method: "POST",
736
- body: JSON.stringify({
737
- kind: $("memory-kind").value,
738
- content,
739
- tags: [],
740
- }),
741
- });
742
- $("memory-content").value = "";
743
- toast("Memory saved");
744
- await refreshAll();
745
- }
746
-
747
- async function createDemoWorkflow() {
748
- await api("/workspace/workflows", {
749
- method: "POST",
750
- body: JSON.stringify({
751
- name: "Upload -> Summarize -> Generate -> Export",
752
- steps: [
753
- { action: "upload" },
754
- { action: "summarize" },
755
- { action: "generate" },
756
- { action: "export" },
757
- ],
758
- }),
759
- });
760
- toast("Workflow created");
761
- await refreshAll();
762
- }
763
-
764
- async function configureComputerMemory(enabled) {
765
- const consent = enabled
766
- ? { approved: true, reason: "Enabled from Workspace OS UI", approved_at: new Date().toISOString() }
767
- : { approved: false };
768
- await api("/workspace/computer-memory", {
769
- method: "POST",
770
- body: JSON.stringify({ enabled, consent }),
771
- });
772
- toast(enabled ? "Computer Memory enabled" : "Computer Memory disabled");
773
- await refreshAll();
774
- }
775
-
776
- function setSkillProgress(name, phase, percent) {
777
- state.skillProgress[name] = { phase, percent };
778
- renderSkills();
779
- }
780
-
781
- function clearSkillProgress(name) {
782
- delete state.skillProgress[name];
783
- renderSkills();
784
- }
785
-
786
- async function runSkillAction(action, skill) {
787
- if (action === "install" || action === "update") {
788
- setSkillProgress(skill, "Download", 24);
789
- await new Promise((resolve) => setTimeout(resolve, 180));
790
- setSkillProgress(skill, "Validate", 68);
791
- }
792
- try {
793
- await api(`/workspace/skills/${action}`, {
794
- method: "POST",
795
- body: JSON.stringify({ skill }),
796
- });
797
- if (action === "install" || action === "update") {
798
- setSkillProgress(skill, "Ready", 100);
799
- await new Promise((resolve) => setTimeout(resolve, 260));
800
- }
801
- toast(`Skill ${action}`);
802
- await refreshAll();
803
- } finally {
804
- clearSkillProgress(skill);
805
- }
806
- }
807
-
808
- document.addEventListener("click", async (event) => {
809
- const entityBtn = event.target.closest("[data-entity]");
810
- if (entityBtn) {
811
- selectEntity(entityBtn.dataset.entity).catch((err) => toast(err.message));
812
- return;
813
- }
814
-
815
- const skillTab = event.target.closest("[data-skill-tab]");
816
- if (skillTab) {
817
- state.skillTab = skillTab.dataset.skillTab;
818
- renderSkills();
819
- return;
820
- }
821
-
822
- const step = event.target.closest("[data-step]");
823
- if (step) {
824
- await api("/workspace/onboarding/step", {
825
- method: "POST",
826
- body: JSON.stringify({ step: step.dataset.step, status: "complete" }),
827
- });
828
- toast("Onboarding step saved");
829
- await refreshAll();
830
- return;
831
- }
832
-
833
- const indexBtn = event.target.closest("[data-index-action]");
834
- if (indexBtn) {
835
- const action = indexBtn.dataset.indexAction;
836
- const source = indexBtn.dataset.source;
837
- await api(`/workspace/indexing/${encodeURIComponent(source)}/${action}`, { method: "POST" });
838
- toast(`Index ${action} complete`);
839
- await refreshAll();
840
- return;
841
- }
842
-
843
- const exportBtn = event.target.closest("[data-export-snapshot]");
844
- if (exportBtn) {
845
- const result = await api(`/workspace/snapshots/${encodeURIComponent(exportBtn.dataset.exportSnapshot)}/export`, { method: "POST" });
846
- toast(`Exported ${result.bytes} bytes`);
847
- return;
848
- }
849
-
850
- const skillBtn = event.target.closest("[data-skill-action]");
851
- if (skillBtn) {
852
- await runSkillAction(skillBtn.dataset.skillAction, skillBtn.dataset.skill);
853
- return;
854
- }
855
-
856
- const wsBtn = event.target.closest("[data-ws-action]");
857
- if (wsBtn) {
858
- const action = wsBtn.dataset.wsAction;
859
- const ws = wsBtn.dataset.ws;
860
- if (action === "activate") {
861
- await activateWorkspace(ws);
862
- } else if (action === "members") {
863
- state.managingWorkspace = ws;
864
- if (state.registry) renderMembers(state.registry.workspaces);
865
- } else if (action === "archive") {
866
- await api(`/workspace/orgs/${encodeURIComponent(ws)}/archive`, { method: "POST" });
867
- toast(`Archived ${ws}`);
868
- await refreshAll();
869
- }
870
- return;
871
- }
872
-
873
- const removeBtn = event.target.closest("[data-member-remove]");
874
- if (removeBtn) {
875
- await api(`/workspace/orgs/${encodeURIComponent(removeBtn.dataset.ws)}/members/${encodeURIComponent(removeBtn.dataset.memberRemove)}`, { method: "DELETE" });
876
- toast("Member removed");
877
- await refreshAll();
878
- }
879
- });
880
-
881
- document.addEventListener("change", async (event) => {
882
- const roleSelect = event.target.closest("[data-member-role]");
883
- if (roleSelect) {
884
- try {
885
- await api(`/workspace/orgs/${encodeURIComponent(roleSelect.dataset.ws)}/members/${encodeURIComponent(roleSelect.dataset.memberRole)}`, {
886
- method: "PATCH",
887
- body: JSON.stringify({ role: roleSelect.value }),
888
- });
889
- toast("Role updated");
890
- await refreshAll();
891
- } catch (err) {
892
- toast(err.message);
893
- }
894
- }
895
- });
896
-
897
- document.addEventListener("DOMContentLoaded", () => {
898
- document.querySelectorAll("[data-workspace-mode]").forEach((button) => {
899
- if (!button.matches("button")) return;
900
- button.addEventListener("click", () => {
901
- const shell = document.querySelector(".workspace-shell");
902
- const adminAvailable = shell?.dataset.adminAvailable === "true";
903
- applyWorkspaceMode(button.dataset.workspaceMode, { adminAvailable });
904
- });
905
- });
906
- const language = $("workspace-language");
907
- if (language) {
908
- language.value = (() => {
909
- try { return localStorage.getItem(LANG_KEY) || "en"; } catch { return "en"; }
910
- })();
911
- language.addEventListener("change", () => {
912
- try { localStorage.setItem(LANG_KEY, language.value); } catch (_) {}
913
- document.documentElement.lang = language.value;
914
- });
915
- }
916
- const logoutButton = $("workspace-logout");
917
- if (logoutButton) logoutButton.addEventListener("click", () => logoutWorkspace());
918
- $("refresh-btn").addEventListener("click", () => refreshAll().catch((err) => toast(err.message)));
919
- $("snapshot-now").addEventListener("click", () => createSnapshot().catch((err) => toast(err.message)));
920
- $("create-snapshot").addEventListener("click", () => createSnapshot().catch((err) => toast(err.message)));
921
- $("complete-onboarding").addEventListener("click", async () => {
922
- await api("/workspace/onboarding/complete", { method: "POST", body: JSON.stringify({ data: { ui: "workspace" } }) });
923
- toast("Onboarding complete");
924
- await refreshAll();
925
- });
926
- $("reload-traces").addEventListener("click", () => refreshAll().catch((err) => toast(err.message)));
927
- $("compare-snapshots").addEventListener("click", () => compareSnapshots().catch((err) => toast(err.message)));
928
- $("save-memory").addEventListener("click", () => saveMemory().catch((err) => toast(err.message)));
929
- $("computer-memory-toggle").addEventListener("change", (event) => configureComputerMemory(event.target.checked).catch((err) => {
930
- event.target.checked = false;
931
- toast(err.message);
932
- }));
933
- $("create-demo-workflow").addEventListener("click", () => createDemoWorkflow().catch((err) => toast(err.message)));
934
- $("reload-skills").addEventListener("click", () => refreshAll().catch((err) => toast(err.message)));
935
- const entitySearch = $("entity-search");
936
- if (entitySearch) entitySearch.addEventListener("input", () => renderEntities());
937
- const reloadEntities = $("reload-entities");
938
- if (reloadEntities) reloadEntities.addEventListener("click", () => loadGraphExplorer().catch((err) => toast(err.message)));
939
- const reloadActivity = $("reload-activity");
940
- if (reloadActivity) reloadActivity.addEventListener("click", () => refreshAll().catch((err) => toast(err.message)));
941
- $("workspace-select").addEventListener("change", (event) => activateWorkspace(event.target.value).catch((err) => toast(err.message)));
942
- $("create-org").addEventListener("click", () => createOrg().catch((err) => toast(err.message)));
943
- $("new-org-btn").addEventListener("click", () => $("org-name").focus());
944
- $("add-member").addEventListener("click", () => {
945
- if (state.managingWorkspace) addMember(state.managingWorkspace).catch((err) => toast(err.message));
946
- });
947
- refreshAll().catch((err) => toast(err.message));
948
- });