ltcai 3.6.0 → 4.0.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.
Files changed (238) hide show
  1. package/README.md +39 -31
  2. package/docs/CHANGELOG.md +64 -0
  3. package/docs/REALTIME_COLLABORATION.md +3 -3
  4. package/docs/V3_FRONTEND.md +9 -8
  5. package/docs/V4_BRAIN_ARCHITECTURE.md +322 -0
  6. package/docs/V4_DIGITAL_BRAIN_RECOVERY.md +552 -0
  7. package/docs/V4_IMPLEMENTATION_PLAN.md +470 -0
  8. package/docs/kg-schema.md +51 -53
  9. package/docs/spec-vs-impl.md +10 -10
  10. package/kg_schema.py +2 -520
  11. package/knowledge_graph.py +37 -4629
  12. package/knowledge_graph_api.py +11 -127
  13. package/latticeai/__init__.py +1 -1
  14. package/latticeai/api/admin.py +16 -17
  15. package/latticeai/api/agents.py +20 -7
  16. package/latticeai/api/auth.py +46 -15
  17. package/latticeai/api/chat.py +112 -76
  18. package/latticeai/api/health.py +1 -1
  19. package/latticeai/api/hooks.py +1 -1
  20. package/latticeai/api/invitations.py +100 -0
  21. package/latticeai/api/knowledge_graph.py +139 -0
  22. package/latticeai/api/local_files.py +1 -1
  23. package/latticeai/api/mcp.py +23 -11
  24. package/latticeai/api/memory.py +1 -1
  25. package/latticeai/api/models.py +1 -1
  26. package/latticeai/api/network.py +81 -0
  27. package/latticeai/api/plugins.py +3 -6
  28. package/latticeai/api/realtime.py +5 -8
  29. package/latticeai/api/search.py +26 -2
  30. package/latticeai/api/security_dashboard.py +2 -3
  31. package/latticeai/api/setup.py +2 -2
  32. package/latticeai/api/static_routes.py +11 -16
  33. package/latticeai/api/tools.py +3 -0
  34. package/latticeai/api/ui_redirects.py +26 -0
  35. package/latticeai/api/workflow_designer.py +85 -6
  36. package/latticeai/api/workspace.py +93 -57
  37. package/latticeai/app_factory.py +1781 -0
  38. package/latticeai/brain/__init__.py +18 -0
  39. package/latticeai/brain/_kg_common.py +1123 -0
  40. package/latticeai/brain/context.py +213 -0
  41. package/latticeai/brain/conversations.py +236 -0
  42. package/latticeai/brain/discovery.py +1455 -0
  43. package/latticeai/brain/documents.py +218 -0
  44. package/latticeai/brain/identity.py +175 -0
  45. package/latticeai/brain/ingest.py +644 -0
  46. package/latticeai/brain/memory.py +102 -0
  47. package/latticeai/brain/network.py +205 -0
  48. package/latticeai/brain/projection.py +561 -0
  49. package/latticeai/brain/provenance.py +401 -0
  50. package/latticeai/brain/retrieval.py +1316 -0
  51. package/latticeai/brain/schema.py +640 -0
  52. package/latticeai/brain/store.py +216 -0
  53. package/latticeai/brain/write_master.py +225 -0
  54. package/latticeai/core/agent.py +31 -7
  55. package/latticeai/core/audit.py +0 -7
  56. package/latticeai/core/config.py +1 -1
  57. package/latticeai/core/context_builder.py +1 -2
  58. package/latticeai/core/enterprise.py +1 -1
  59. package/latticeai/core/graph_curator.py +2 -2
  60. package/latticeai/core/invitations.py +131 -0
  61. package/latticeai/core/marketplace.py +1 -1
  62. package/latticeai/core/mcp_registry.py +791 -0
  63. package/latticeai/core/model_compat.py +1 -1
  64. package/latticeai/core/model_resolution.py +0 -1
  65. package/latticeai/core/multi_agent.py +238 -4
  66. package/latticeai/core/policy.py +54 -0
  67. package/latticeai/core/realtime.py +65 -44
  68. package/latticeai/core/security.py +1 -1
  69. package/latticeai/core/sessions.py +66 -10
  70. package/latticeai/core/users.py +147 -0
  71. package/latticeai/core/workflow_engine.py +114 -2
  72. package/latticeai/core/workspace_os.py +477 -29
  73. package/latticeai/models/__init__.py +7 -0
  74. package/latticeai/models/router.py +779 -0
  75. package/latticeai/server_app.py +29 -1536
  76. package/latticeai/services/agent_runtime.py +243 -4
  77. package/latticeai/services/app_context.py +75 -14
  78. package/latticeai/services/ingestion.py +47 -0
  79. package/latticeai/services/kg_portability.py +33 -3
  80. package/latticeai/services/memory_service.py +39 -11
  81. package/latticeai/services/model_runtime.py +2 -5
  82. package/latticeai/services/platform_runtime.py +100 -23
  83. package/latticeai/services/run_executor.py +328 -0
  84. package/latticeai/services/search_service.py +17 -8
  85. package/latticeai/services/tool_dispatch.py +12 -2
  86. package/latticeai/services/triggers.py +241 -0
  87. package/latticeai/services/upload_service.py +37 -12
  88. package/latticeai/services/workspace_service.py +55 -16
  89. package/llm_router.py +29 -772
  90. package/ltcai_cli.py +1 -2
  91. package/mcp_registry.py +25 -788
  92. package/p_reinforce.py +124 -14
  93. package/package.json +10 -20
  94. package/scripts/bump_version.py +99 -0
  95. package/scripts/generate_diagrams.py +0 -1
  96. package/scripts/lint_v3.mjs +105 -18
  97. package/scripts/validate_release_artifacts.py +0 -1
  98. package/scripts/wheel_smoke.py +142 -0
  99. package/server.py +11 -7
  100. package/setup_wizard.py +1142 -0
  101. package/static/sw.js +81 -52
  102. package/static/v3/asset-manifest.json +33 -25
  103. package/static/v3/css/{lattice.base.e4cdd05d.css → lattice.base.49deefb5.css} +1 -1
  104. package/static/v3/css/lattice.base.css +1 -1
  105. package/static/v3/css/{lattice.components.9b49d614.css → lattice.components.cde18231.css} +1 -1
  106. package/static/v3/css/lattice.components.css +1 -1
  107. package/static/v3/css/{lattice.shell.8fcc9d33.css → lattice.shell.29d36d85.css} +1 -1
  108. package/static/v3/css/lattice.shell.css +1 -1
  109. package/static/v3/css/{lattice.tokens.e7018963.css → lattice.tokens.304cbc40.css} +3 -0
  110. package/static/v3/css/lattice.tokens.css +3 -0
  111. package/static/v3/css/{lattice.views.22f69117.css → lattice.views.0a18b6c5.css} +2 -2
  112. package/static/v3/css/lattice.views.css +2 -2
  113. package/static/v3/index.html +3 -4
  114. package/static/v3/js/{app.c541f955.js → app.c5c80c46.js} +1 -1
  115. package/static/v3/js/core/{api.33d6320e.js → api.ba0fbf14.js} +58 -1
  116. package/static/v3/js/core/api.js +57 -0
  117. package/static/v3/js/core/i18n.880e1fec.js +575 -0
  118. package/static/v3/js/core/i18n.js +575 -0
  119. package/static/v3/js/core/routes.37522821.js +101 -0
  120. package/static/v3/js/core/routes.js +71 -63
  121. package/static/v3/js/core/{shell.8c163e0e.js → shell.e3f6bbfa.js} +68 -39
  122. package/static/v3/js/core/shell.js +66 -37
  123. package/static/v3/js/core/{store.34ebd5e6.js → store.7b2aa044.js} +11 -1
  124. package/static/v3/js/core/store.js +11 -1
  125. package/static/v3/js/views/account.eff40715.js +143 -0
  126. package/static/v3/js/views/account.js +143 -0
  127. package/static/v3/js/views/activity.0d271ef9.js +67 -0
  128. package/static/v3/js/views/activity.js +67 -0
  129. package/static/v3/js/views/{admin-users.03bac88c.js → admin-users.f7ac7b43.js} +4 -6
  130. package/static/v3/js/views/admin-users.js +4 -6
  131. package/static/v3/js/views/{agents.014d0b74.js → agents.17c5288d.js} +35 -12
  132. package/static/v3/js/views/agents.js +35 -12
  133. package/static/v3/js/views/{chat.e6dd7dd0.js → chat.e250e2cc.js} +23 -0
  134. package/static/v3/js/views/chat.js +23 -0
  135. package/static/v3/js/views/graph-canvas.17c15d65.js +509 -0
  136. package/static/v3/js/views/graph-canvas.js +509 -0
  137. package/static/v3/js/views/{hybrid-search.b22b97e0.js → hybrid-search.2fb63ed9.js} +1 -2
  138. package/static/v3/js/views/hybrid-search.js +1 -2
  139. package/static/v3/js/views/{knowledge-graph.a96040a5.js → knowledge-graph.4d09c537.js} +60 -44
  140. package/static/v3/js/views/knowledge-graph.js +60 -44
  141. package/static/v3/js/views/network.52a4f181.js +97 -0
  142. package/static/v3/js/views/network.js +97 -0
  143. package/static/v3/js/views/{planning.9ac3e313.js → planning.4876fd77.js} +26 -5
  144. package/static/v3/js/views/planning.js +26 -5
  145. package/static/v3/js/views/runs.b63b2afa.js +144 -0
  146. package/static/v3/js/views/runs.js +144 -0
  147. package/static/v3/js/views/{settings.8631fa5e.js → settings.b7140634.js} +7 -8
  148. package/static/v3/js/views/settings.js +7 -8
  149. package/static/v3/js/views/snapshots.6f5db095.js +135 -0
  150. package/static/v3/js/views/snapshots.js +135 -0
  151. package/static/v3/js/views/{workflows.26c57290.js → workflows.7752225a.js} +87 -2
  152. package/static/v3/js/views/workflows.js +87 -2
  153. package/static/v3/js/views/workspace-admin.c466029b.js +156 -0
  154. package/static/v3/js/views/workspace-admin.js +156 -0
  155. package/static/vendor/chart.umd.min.js +20 -0
  156. package/static/vendor/fonts/inter-latin-300-normal.woff2 +0 -0
  157. package/static/vendor/fonts/inter-latin-400-normal.woff2 +0 -0
  158. package/static/vendor/fonts/inter-latin-500-normal.woff2 +0 -0
  159. package/static/vendor/fonts/inter-latin-600-normal.woff2 +0 -0
  160. package/static/vendor/fonts/inter-latin-700-normal.woff2 +0 -0
  161. package/static/vendor/fonts/inter-latin-800-normal.woff2 +0 -0
  162. package/static/vendor/fonts/inter.css +44 -0
  163. package/static/vendor/icons/tabler-icons.min.css +4 -0
  164. package/static/vendor/icons/tabler-icons.woff2 +0 -0
  165. package/static/vendor/marked.min.js +69 -0
  166. package/telegram_bot.py +1 -2
  167. package/tools/commands.py +4 -2
  168. package/tools/computer.py +1 -1
  169. package/tools/documents.py +1 -3
  170. package/tools/filesystem.py +0 -4
  171. package/tools/knowledge.py +1 -3
  172. package/tools/network.py +1 -3
  173. package/codex_telegram_bot.py +0 -195
  174. package/docs/assets/v3.4.0/agent-run.png +0 -0
  175. package/docs/assets/v3.4.0/agents.png +0 -0
  176. package/docs/assets/v3.4.0/before/chat-before.png +0 -0
  177. package/docs/assets/v3.4.0/before/files-before.png +0 -0
  178. package/docs/assets/v3.4.0/chat.png +0 -0
  179. package/docs/assets/v3.4.0/connect-folder.png +0 -0
  180. package/docs/assets/v3.4.0/files.png +0 -0
  181. package/docs/assets/v3.4.0/home.png +0 -0
  182. package/docs/assets/v3.4.0/hooks-dispatch.png +0 -0
  183. package/docs/assets/v3.4.0/knowledge-graph.png +0 -0
  184. package/docs/assets/v3.4.0/local-agent.png +0 -0
  185. package/docs/assets/v3.4.0/memory.png +0 -0
  186. package/docs/assets/v3.4.0/settings.png +0 -0
  187. package/docs/assets/v3.4.0/vision-input.png +0 -0
  188. package/docs/assets/v3.4.0/workflows.png +0 -0
  189. package/docs/assets/v3.4.1/e2e_runtime_log.txt +0 -42
  190. package/docs/assets/v3.4.1/hooks-dispatch.png +0 -0
  191. package/docs/assets/v3.4.1/local-agent.png +0 -0
  192. package/docs/images/admin-dashboard.png +0 -0
  193. package/docs/images/architecture.png +0 -0
  194. package/docs/images/enterprise.png +0 -0
  195. package/docs/images/graph.png +0 -0
  196. package/docs/images/hero.gif +0 -0
  197. package/docs/images/knowledge-graph.png +0 -0
  198. package/docs/images/lattice-ai-demo.gif +0 -0
  199. package/docs/images/lattice-ai-hero.png +0 -0
  200. package/docs/images/logo.svg +0 -33
  201. package/docs/images/mobile-responsive.png +0 -0
  202. package/docs/images/model-recommendation.png +0 -0
  203. package/docs/images/onboarding.png +0 -0
  204. package/docs/images/organization.png +0 -0
  205. package/docs/images/pipeline.png +0 -0
  206. package/docs/images/screenshot-admin.png +0 -0
  207. package/docs/images/screenshot-chat.png +0 -0
  208. package/docs/images/screenshot-graph.png +0 -0
  209. package/docs/images/skills.png +0 -0
  210. package/docs/images/workspace-dark.png +0 -0
  211. package/docs/images/workspace-light.png +0 -0
  212. package/docs/images/workspace.png +0 -0
  213. package/requirements.txt +0 -16
  214. package/static/account.html +0 -115
  215. package/static/activity.html +0 -73
  216. package/static/admin.html +0 -488
  217. package/static/agents.html +0 -139
  218. package/static/chat.html +0 -844
  219. package/static/css/reference/account.css +0 -439
  220. package/static/css/reference/admin.css +0 -610
  221. package/static/css/reference/base.css +0 -1661
  222. package/static/css/reference/chat.css +0 -4623
  223. package/static/css/reference/graph.css +0 -1016
  224. package/static/css/responsive.css +0 -861
  225. package/static/graph.html +0 -124
  226. package/static/platform.css +0 -104
  227. package/static/plugins.html +0 -136
  228. package/static/scripts/account.js +0 -238
  229. package/static/scripts/admin.js +0 -1614
  230. package/static/scripts/chat.js +0 -5081
  231. package/static/scripts/graph.js +0 -1804
  232. package/static/scripts/platform.js +0 -64
  233. package/static/scripts/ux.js +0 -167
  234. package/static/scripts/workspace.js +0 -948
  235. package/static/v3/js/core/routes.2ce3815a.js +0 -93
  236. package/static/workflows.html +0 -146
  237. package/static/workspace.css +0 -1121
  238. 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
- });