ltcai 1.5.0 → 1.7.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.
@@ -6,8 +6,16 @@ const state = {
6
6
  activeWorkspace: null,
7
7
  registry: null,
8
8
  managingWorkspace: null,
9
+ skillsPayload: null,
10
+ skillTab: "recommended",
11
+ skillProgress: {},
12
+ entities: [],
13
+ activeEntity: null,
9
14
  };
10
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
+
11
19
  function $(id) {
12
20
  return document.getElementById(id);
13
21
  }
@@ -63,6 +71,56 @@ function renderMetrics(os) {
63
71
  `).join("");
64
72
  }
65
73
 
74
+ function latestTimestamp(...groups) {
75
+ const values = groups.flat().filter(Boolean).map((value) => {
76
+ const stamp = new Date(value);
77
+ return Number.isNaN(stamp.getTime()) ? null : stamp;
78
+ }).filter(Boolean);
79
+ if (!values.length) return "";
80
+ return new Date(Math.max(...values.map((stamp) => stamp.getTime()))).toISOString().slice(0, 19).replace("T", " ");
81
+ }
82
+
83
+ function renderWorkspaceHealth({ os, indexing, skills, timeline }) {
84
+ const counts = os?.counts || {};
85
+ const graph = os?.graph || {};
86
+ const nodes = Object.values(graph.nodes || {}).reduce((sum, value) => sum + Number(value || 0), 0);
87
+ const edges = Object.values(graph.edges || {}).reduce((sum, value) => sum + Number(value || 0), 0);
88
+ const sources = indexing?.sources || [];
89
+ const indexedFiles = sources.reduce((sum, source) => {
90
+ const fileStatus = source.file_status || {};
91
+ return sum + Number(fileStatus.indexed ?? source.success_count ?? 0);
92
+ }, 0);
93
+ const sourceTimes = sources.flatMap((source) => [source.last_run_at, source.last_scanned_at, source.updated_at]);
94
+ const eventTimes = (timeline?.events || []).slice(0, 10).map((event) => event.timestamp);
95
+ const currentModel = os?.models?.current_model || os?.models?.local_model || os?.models?.public_model || "not loaded";
96
+ const status = nodes || indexedFiles || counts.memories || counts.agent_runs ? "ready" : "empty";
97
+ const statusEl = $("workspace-health-status");
98
+ if (statusEl) {
99
+ statusEl.textContent = status;
100
+ statusEl.className = `status-pill ${status === "ready" ? "status-complete" : "status-running"}`;
101
+ }
102
+ const items = [
103
+ ["Indexed Files", indexedFiles, "ti-files", sources.length ? `${sources.length} source(s)` : "No indexed sources"],
104
+ ["Graph Nodes", nodes, "ti-chart-dots-3", `${edges.toLocaleString()} relationship(s)`],
105
+ ["Graph Relationships", edges, "ti-git-branch", "Knowledge links"],
106
+ ["Installed Skills", skills?.total_installed ?? counts.skills ?? 0, "ti-puzzle", `${skills?.total_available ?? 0} available`],
107
+ ["Memory Entries", counts.memories || 0, "ti-book-2", "Workspace memory"],
108
+ ["Agent Runs", counts.agent_runs || 0, "ti-route-alt-left", `${counts.workflows || 0} workflow(s)`],
109
+ ["Current Model", currentModel, "ti-cpu", `${(os?.models?.loaded_models || []).length} loaded`],
110
+ ["Last Sync Time", latestTimestamp(os?.updated_at, sourceTimes, eventTimes) || "not synced", "ti-clock", `v${os?.version || "unknown"}`],
111
+ ];
112
+ const grid = $("workspace-health-grid");
113
+ if (!grid) return;
114
+ grid.innerHTML = items.map(([label, value, icon, meta]) => `
115
+ <div class="health-card">
116
+ <i class="ti ${icon}"></i>
117
+ <span>${escapeHtml(label)}</span>
118
+ <strong>${escapeHtml(value)}</strong>
119
+ <em>${escapeHtml(meta)}</em>
120
+ </div>
121
+ `).join("");
122
+ }
123
+
66
124
  function renderOnboarding(payload) {
67
125
  const steps = payload.steps || [];
68
126
  $("onboarding-steps").innerHTML = steps.map((step) => {
@@ -194,30 +252,92 @@ function renderWorkflows(payload) {
194
252
  `).join("") : `<div class="list-item"><div class="meta-line">No workflows.</div></div>`;
195
253
  }
196
254
 
197
- function renderSkills(payload) {
255
+ function skillName(skill) {
256
+ return skill.skill || skill.name || "skill";
257
+ }
258
+
259
+ function skillProgress(name) {
260
+ return state.skillProgress[name] || null;
261
+ }
262
+
263
+ // Compute the four marketplace tabs from the registry payload (machine-global
264
+ // registry + locally-installed state). "Updates" = installed skills whose
265
+ // registry version differs from the installed version.
266
+ function computeSkillTabs(payload) {
198
267
  const installed = payload.installed || [];
199
- const available = (payload.available || []).filter((skill) => !installed.some((item) => item.name === (skill.skill || skill.name))).slice(0, 8);
200
- const rows = [
201
- ...installed.map((skill) => ({ ...skill, marketplace: false })),
202
- ...available.map((skill) => ({ name: skill.skill || skill.name, description: skill.description, version: skill.version || "remote", enabled: skill.enabled, marketplace: true })),
203
- ];
204
- $("skill-list").innerHTML = rows.length ? rows.map((skill) => `
268
+ const available = payload.available || [];
269
+ const installedNames = new Set(installed.map(skillName));
270
+ const notInstalled = available.filter((s) => !installedNames.has(skillName(s)));
271
+ const availByName = new Map(available.map((s) => [skillName(s), s]));
272
+ const updates = installed.filter((s) => {
273
+ const remote = availByName.get(skillName(s));
274
+ return remote && remote.version && s.version && remote.version !== s.version;
275
+ });
276
+ const recommended = notInstalled.filter((s) => {
277
+ const hay = `${skillName(s)} ${s.category || ""} ${s.description || ""}`.toLowerCase();
278
+ return RECOMMENDED_SKILL_HINTS.some((h) => hay.includes(h));
279
+ });
280
+ const popular = notInstalled.slice().sort((a, b) => Number(b.downloads || b.popularity || 0) - Number(a.downloads || a.popularity || 0));
281
+ return { installed, popular, recommended: recommended.length ? recommended : popular.slice(0, 8), updates };
282
+ }
283
+
284
+ function renderSkillRow(skill, { installed }) {
285
+ const name = skillName(skill);
286
+ const enabled = skill.enabled !== false;
287
+ const version = skill.version || (installed ? "local" : "registry");
288
+ const source = skill.plugin || skill.source || skill.source_url || (installed ? "installed" : "marketplace");
289
+ const validation = skill.validation_status || (installed ? "ready" : "not installed");
290
+ const installStatus = skill.install_status || (installed ? "ready" : "available");
291
+ const progress = skillProgress(name);
292
+ const actions = installed
293
+ ? `<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>
294
+ <button class="small-action" data-skill-action="update" data-skill="${escapeHtml(name)}"><i class="ti ti-refresh"></i>Update</button>`
295
+ : `<button class="small-action" data-skill-action="install" data-skill="${escapeHtml(name)}" ${progress ? "disabled" : ""}><i class="ti ti-download"></i>Install</button>`;
296
+ const progressHtml = progress ? `
297
+ <div class="skill-progress" aria-label="Install progress">
298
+ <div class="skill-progress-head"><span>${escapeHtml(progress.phase)}</span><span>${escapeHtml(progress.percent)}%</span></div>
299
+ <div class="skill-progress-track"><span style="width:${Math.max(0, Math.min(100, progress.percent))}%"></span></div>
300
+ </div>
301
+ ` : "";
302
+ return `
205
303
  <div class="list-item">
206
304
  <div class="list-title">
207
- <span>${escapeHtml(skill.name)}</span>
208
- <span class="status-pill ${skill.enabled === false ? "status-failed" : "status-complete"}">${skill.enabled === false ? "disabled" : "enabled"}</span>
305
+ <span>${escapeHtml(name)}</span>
306
+ <span class="status-pill ${installed ? (enabled ? "status-complete" : "status-failed") : ""}">${installed ? (enabled ? "enabled" : "disabled") : "available"}</span>
209
307
  </div>
210
- <div class="meta-line">${escapeHtml(skill.description || "")}</div>
308
+ <div class="meta-line">${escapeHtml(skill.description || "No description")}</div>
211
309
  <div class="tag-row">
212
- <span class="tag">${escapeHtml(skill.version || "local")}</span>
213
- <span class="tag">${skill.marketplace ? "marketplace" : "installed"}</span>
310
+ <span class="tag">v${escapeHtml(version)}</span>
311
+ ${skill.category ? `<span class="tag">${escapeHtml(skill.category)}</span>` : ""}
312
+ <span class="tag">${escapeHtml(source)}</span>
313
+ <span class="tag">install: ${escapeHtml(installStatus)}</span>
314
+ <span class="tag">validation: ${escapeHtml(validation)}</span>
214
315
  </div>
215
- <div class="item-actions">
216
- <button class="small-action" data-skill-action="enable" data-skill="${escapeHtml(skill.name)}"><i class="ti ti-toggle-right"></i>Enable</button>
217
- <button class="small-action" data-skill-action="disable" data-skill="${escapeHtml(skill.name)}"><i class="ti ti-toggle-left"></i>Disable</button>
218
- </div>
219
- </div>
220
- `).join("") : `<div class="list-item"><div class="meta-line">No skills found.</div></div>`;
316
+ ${progressHtml}
317
+ <div class="item-actions">${actions}</div>
318
+ </div>`;
319
+ }
320
+
321
+ function renderSkills(payload) {
322
+ if (payload) state.skillsPayload = payload;
323
+ const data = state.skillsPayload || { installed: [], available: [] };
324
+ const tabs = computeSkillTabs(data);
325
+ const updatesCount = $("skill-updates-count");
326
+ if (updatesCount) updatesCount.textContent = tabs.updates.length ? String(tabs.updates.length) : "";
327
+ document.querySelectorAll("[data-skill-tab]").forEach((btn) => {
328
+ btn.classList.toggle("active", btn.dataset.skillTab === state.skillTab);
329
+ });
330
+ const tab = state.skillTab;
331
+ const rows = (tab === "installed" || tab === "updates")
332
+ ? (tabs[tab] || []).map((s) => renderSkillRow(s, { installed: true }))
333
+ : (tabs[tab] || []).slice(0, 24).map((s) => renderSkillRow(s, { installed: false }));
334
+ const empty = {
335
+ recommended: "No recommended skills right now.",
336
+ popular: "Marketplace is empty.",
337
+ installed: "No skills installed yet.",
338
+ updates: "All installed skills are up to date.",
339
+ }[tab];
340
+ $("skill-list").innerHTML = rows.length ? rows.join("") : `<div class="list-item"><div class="meta-line">${escapeHtml(empty)}</div></div>`;
221
341
  }
222
342
 
223
343
  function renderTimeline(payload) {
@@ -327,6 +447,173 @@ async function addMember(workspaceId) {
327
447
  await refreshAll();
328
448
  }
329
449
 
450
+ // ── Workspace summary (Phase 3) ──────────────────────────────────────────────
451
+ function renderWorkspaceSummary(os) {
452
+ const reg = os?.workspace_registry || {};
453
+ const workspaces = reg.workspaces || [];
454
+ const activeId = state.activeWorkspace || reg.active_workspace;
455
+ const active = workspaces.find((w) => w.workspace_id === activeId) || workspaces[0] || { name: "Personal Workspace", type: "personal", your_role: "owner", member_count: 1 };
456
+ const counts = os?.counts || {};
457
+ const scopePill = $("summary-scope-pill");
458
+ if (scopePill) scopePill.textContent = active.type || "personal";
459
+ const summary = $("workspace-summary");
460
+ if (summary) {
461
+ const stats = [["Snapshots", counts.snapshots], ["Memories", counts.memories], ["Agent runs", counts.agent_runs], ["Workflows", counts.workflows], ["Traces", counts.traces], ["Timeline", counts.timeline]];
462
+ summary.innerHTML = `
463
+ <div class="summary-main">
464
+ <div class="summary-icon"><i class="ti ${active.type === "organization" ? "ti-building-community" : "ti-user"}"></i></div>
465
+ <div class="summary-id">
466
+ <div class="summary-name">${escapeHtml(active.name || "Personal Workspace")}</div>
467
+ <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>
468
+ </div>
469
+ </div>
470
+ <div class="summary-stats">
471
+ ${stats.map(([l, v]) => `<div class="summary-stat"><strong>${escapeHtml(v || 0)}</strong><span>${escapeHtml(l)}</span></div>`).join("")}
472
+ </div>`;
473
+ }
474
+ const quick = $("workspace-quickswitch");
475
+ if (quick) {
476
+ quick.innerHTML = workspaces.map((w) => `
477
+ <button class="switch-chip ${w.workspace_id === activeId ? "active" : ""}" data-ws-action="activate" data-ws="${escapeHtml(w.workspace_id)}">
478
+ <i class="ti ${w.type === "organization" ? "ti-building-community" : "ti-user"}"></i>
479
+ <span>${escapeHtml(w.name)}</span>${w.workspace_id === activeId ? ' <i class="ti ti-check"></i>' : ""}
480
+ </button>`).join("");
481
+ }
482
+ }
483
+
484
+ // ── Knowledge Graph explorer (Phase 2) ───────────────────────────────────────
485
+ 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" };
486
+ function entityIcon(type) { return ENTITY_ICONS[type] || "ti-point"; }
487
+ function prettyId(id) { return String(id || "").split(":").slice(1).join(":") || String(id || ""); }
488
+
489
+ async function loadGraphExplorer() {
490
+ try {
491
+ const data = await api("/knowledge-graph/graph?limit=150");
492
+ const nodes = (data.nodes || []).slice();
493
+ nodes.sort((a, b) => (b.importance ?? b.metadata?.graph_metrics?.importance_raw ?? 0) - (a.importance ?? a.metadata?.graph_metrics?.importance_raw ?? 0));
494
+ state.entities = nodes;
495
+ renderEntities();
496
+ } catch (e) {
497
+ const el = $("entity-list");
498
+ if (el) el.innerHTML = `<div class="list-item"><div class="meta-line">Knowledge graph unavailable: ${escapeHtml(e.message)}</div></div>`;
499
+ }
500
+ }
501
+
502
+ function renderEntities() {
503
+ const el = $("entity-list");
504
+ if (!el) return;
505
+ const q = ($("entity-search")?.value || "").toLowerCase().trim();
506
+ const filtered = q ? state.entities.filter((n) => `${n.title || ""} ${n.type || ""} ${n.id || ""}`.toLowerCase().includes(q)) : state.entities;
507
+ const list = filtered.slice(0, 40);
508
+ el.innerHTML = list.length ? list.map((n) => {
509
+ const m = n.metadata?.graph_metrics || {};
510
+ const imp = Math.round((n.importance_norm ?? m.importance_norm ?? 0) * 100);
511
+ return `
512
+ <button class="list-item entity-card ${n.id === state.activeEntity ? "selected" : ""}" data-entity="${escapeHtml(n.id)}">
513
+ <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>
514
+ ${n.summary ? `<div class="meta-line">${escapeHtml(String(n.summary).slice(0, 110))}</div>` : ""}
515
+ <div class="tag-row"><span class="tag">${escapeHtml(m.degree ?? 0)} links</span><span class="tag">importance ${imp}%</span></div>
516
+ <div class="importance-bar"><span style="width:${imp}%"></span></div>
517
+ </button>`;
518
+ }).join("") : `<div class="list-item"><div class="meta-line">No matching entities.</div></div>`;
519
+ }
520
+
521
+ async function selectEntity(id) {
522
+ state.activeEntity = id;
523
+ renderEntities();
524
+ const detail = $("entity-detail");
525
+ const title = $("entity-detail-title");
526
+ if (title) title.textContent = "Loading…";
527
+ try {
528
+ const d = await api(`/workspace/relationships/${encodeURIComponent(id)}`);
529
+ const node = d.node || {};
530
+ const related = d.related_entities || [];
531
+ const relMap = new Map(related.map((r) => [r.id, r]));
532
+ const labelFor = (nodeId) => { const r = relMap.get(nodeId); return r ? (r.title || prettyId(nodeId)) : prettyId(nodeId); };
533
+ const edgeRow = (e, dir) => {
534
+ const other = dir === "out" ? e.to : e.from;
535
+ 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>`;
536
+ };
537
+ const inbound = (d.inbound || []).slice(0, 8);
538
+ const outbound = (d.outbound || []).slice(0, 8);
539
+ const path = Array.isArray(d.shortest_path) ? d.shortest_path : [];
540
+ if (title) title.textContent = node.title || prettyId(id);
541
+ detail.innerHTML = `
542
+ <div class="list-item">
543
+ <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>
544
+ ${node.summary ? `<div class="meta-line">${escapeHtml(node.summary)}</div>` : ""}
545
+ <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>
546
+ </div>
547
+ <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>
548
+ <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>
549
+ ${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>` : ""}
550
+ ${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>` : ""}
551
+ <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>`;
552
+ } catch (e) {
553
+ if (title) title.textContent = "Relationships";
554
+ detail.innerHTML = `<div class="list-item"><div class="meta-line">No relationships available: ${escapeHtml(e.message)}</div></div>`;
555
+ }
556
+ }
557
+
558
+ // ── Recent activity feed (Phase 2), built from already-fetched data ───────────
559
+ function renderActivity({ traces, snapshots, memories, workflows, timeline }) {
560
+ const items = [];
561
+ (traces.traces || []).forEach((t) => items.push({ ts: t.created_at, icon: "ti-search", label: `Answer trace: ${t.question || "query"}`, tag: "graph rag" }));
562
+ (snapshots.snapshots || []).forEach((s) => items.push({ ts: s.created_at, icon: "ti-stack-2", label: `Snapshot: ${s.name}`, tag: "snapshot" }));
563
+ (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" }));
564
+ (workflows.workflows || []).forEach((w) => items.push({ ts: w.created_at, icon: "ti-git-branch", label: `Workflow: ${w.name}`, tag: "workflow" }));
565
+ (timeline.events || []).forEach((e) => items.push({ ts: e.timestamp, icon: "ti-timeline-event", label: e.event_type || "event", tag: e.area || "workspace" }));
566
+ items.sort((a, b) => String(b.ts || "").localeCompare(String(a.ts || "")));
567
+ const el = $("activity-list");
568
+ if (!el) return;
569
+ el.innerHTML = items.length ? items.slice(0, 18).map((it) => `
570
+ <div class="list-item activity-item">
571
+ <div class="list-title"><span><i class="ti ${it.icon}"></i> ${escapeHtml(it.label)}</span><span class="status-pill">${escapeHtml(it.tag)}</span></div>
572
+ <div class="meta-line">${escapeHtml(it.ts || "")}</div>
573
+ </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>`;
574
+ }
575
+
576
+ function renderMemoryFeed(payload) {
577
+ const memories = payload.memories || [];
578
+ const el = $("memory-feed");
579
+ if (!el) return;
580
+ el.innerHTML = memories.length ? memories.slice(0, 8).map((m) => `
581
+ <div class="list-item">
582
+ <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>
583
+ <div class="meta-line">${escapeHtml(String(m.content || "").slice(0, 140))}</div>
584
+ </div>`).join("") : `<div class="list-item"><div class="meta-line">No workspace memory yet.</div></div>`;
585
+ }
586
+
587
+ // ── Enterprise capability panel (Phase 6) ─────────────────────────────────────
588
+ const CAPABILITY_LABELS = {
589
+ sso_advanced: "Advanced SSO", idp_provisioning: "IdP Provisioning", scim: "SCIM",
590
+ rbac_abac_advanced: "Advanced RBAC/ABAC", tenant_isolation: "Tenant Isolation",
591
+ compliance_retention: "Compliance Retention", siem_export: "SIEM Export",
592
+ private_vpc: "Private VPC", air_gapped_deployment: "Air-gapped Deploy",
593
+ dlp_policy: "DLP Policy", ediscovery: "eDiscovery", admin_policy_packs: "Admin Policy Packs",
594
+ };
595
+ function renderEnterprise(edition) {
596
+ edition = edition || {};
597
+ const caps = edition.capabilities || {};
598
+ const editionName = edition.edition || "community";
599
+ const pill = $("enterprise-edition-pill");
600
+ if (pill) { pill.textContent = editionName; pill.className = `status-pill ${edition.is_enterprise ? "status-complete" : ""}`; }
601
+ const note = $("enterprise-note");
602
+ 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.";
603
+ const grid = $("capability-grid");
604
+ if (!grid) return;
605
+ const keys = Object.keys(caps).length ? Object.keys(caps) : Object.keys(CAPABILITY_LABELS);
606
+ grid.innerHTML = keys.map((k) => {
607
+ const on = Boolean(caps[k]);
608
+ return `
609
+ <div class="capability-card ${on ? "on" : "off"}">
610
+ <i class="ti ${on ? "ti-circle-check" : "ti-lock"}"></i>
611
+ <span class="cap-name">${escapeHtml(CAPABILITY_LABELS[k] || k)}</span>
612
+ <span class="status-pill ${on ? "status-complete" : "status-failed"}">${on ? "enabled" : "disabled"}</span>
613
+ </div>`;
614
+ }).join("");
615
+ }
616
+
330
617
  async function refreshAll() {
331
618
  const [os, onboarding, traces, indexing, snapshots, memories, computerMemory, agents, workflows, skills, timeline] = await Promise.all([
332
619
  api("/workspace/os"),
@@ -343,6 +630,7 @@ async function refreshAll() {
343
630
  ]);
344
631
  state.os = os;
345
632
  renderMetrics(os);
633
+ renderWorkspaceHealth({ os, indexing, skills, timeline });
346
634
  if (os.workspace_registry) renderWorkspaceRegistry(os.workspace_registry, os.edition);
347
635
  renderOnboarding(onboarding);
348
636
  renderTraces(traces);
@@ -354,6 +642,11 @@ async function refreshAll() {
354
642
  renderWorkflows(workflows);
355
643
  renderSkills(skills);
356
644
  renderTimeline(timeline);
645
+ renderWorkspaceSummary(os);
646
+ renderEnterprise(os.edition);
647
+ renderActivity({ traces, snapshots, memories, workflows, timeline });
648
+ renderMemoryFeed(memories);
649
+ loadGraphExplorer();
357
650
  }
358
651
 
359
652
  async function createSnapshot() {
@@ -422,7 +715,52 @@ async function configureComputerMemory(enabled) {
422
715
  await refreshAll();
423
716
  }
424
717
 
718
+ function setSkillProgress(name, phase, percent) {
719
+ state.skillProgress[name] = { phase, percent };
720
+ renderSkills();
721
+ }
722
+
723
+ function clearSkillProgress(name) {
724
+ delete state.skillProgress[name];
725
+ renderSkills();
726
+ }
727
+
728
+ async function runSkillAction(action, skill) {
729
+ if (action === "install" || action === "update") {
730
+ setSkillProgress(skill, "Download", 24);
731
+ await new Promise((resolve) => setTimeout(resolve, 180));
732
+ setSkillProgress(skill, "Validate", 68);
733
+ }
734
+ try {
735
+ await api(`/workspace/skills/${action}`, {
736
+ method: "POST",
737
+ body: JSON.stringify({ skill }),
738
+ });
739
+ if (action === "install" || action === "update") {
740
+ setSkillProgress(skill, "Ready", 100);
741
+ await new Promise((resolve) => setTimeout(resolve, 260));
742
+ }
743
+ toast(`Skill ${action}`);
744
+ await refreshAll();
745
+ } finally {
746
+ clearSkillProgress(skill);
747
+ }
748
+ }
749
+
425
750
  document.addEventListener("click", async (event) => {
751
+ const entityBtn = event.target.closest("[data-entity]");
752
+ if (entityBtn) {
753
+ selectEntity(entityBtn.dataset.entity).catch((err) => toast(err.message));
754
+ return;
755
+ }
756
+
757
+ const skillTab = event.target.closest("[data-skill-tab]");
758
+ if (skillTab) {
759
+ state.skillTab = skillTab.dataset.skillTab;
760
+ renderSkills();
761
+ return;
762
+ }
763
+
426
764
  const step = event.target.closest("[data-step]");
427
765
  if (step) {
428
766
  await api("/workspace/onboarding/step", {
@@ -453,12 +791,7 @@ document.addEventListener("click", async (event) => {
453
791
 
454
792
  const skillBtn = event.target.closest("[data-skill-action]");
455
793
  if (skillBtn) {
456
- await api(`/workspace/skills/${skillBtn.dataset.skillAction}`, {
457
- method: "POST",
458
- body: JSON.stringify({ skill: skillBtn.dataset.skill }),
459
- });
460
- toast(`Skill ${skillBtn.dataset.skillAction}`);
461
- await refreshAll();
794
+ await runSkillAction(skillBtn.dataset.skillAction, skillBtn.dataset.skill);
462
795
  return;
463
796
  }
464
797
 
@@ -521,6 +854,12 @@ document.addEventListener("DOMContentLoaded", () => {
521
854
  }));
522
855
  $("create-demo-workflow").addEventListener("click", () => createDemoWorkflow().catch((err) => toast(err.message)));
523
856
  $("reload-skills").addEventListener("click", () => refreshAll().catch((err) => toast(err.message)));
857
+ const entitySearch = $("entity-search");
858
+ if (entitySearch) entitySearch.addEventListener("input", () => renderEntities());
859
+ const reloadEntities = $("reload-entities");
860
+ if (reloadEntities) reloadEntities.addEventListener("click", () => loadGraphExplorer().catch((err) => toast(err.message)));
861
+ const reloadActivity = $("reload-activity");
862
+ if (reloadActivity) reloadActivity.addEventListener("click", () => refreshAll().catch((err) => toast(err.message)));
524
863
  $("workspace-select").addEventListener("change", (event) => activateWorkspace(event.target.value).catch((err) => toast(err.message)));
525
864
  $("create-org").addEventListener("click", () => createOrg().catch((err) => toast(err.message)));
526
865
  $("new-org-btn").addEventListener("click", () => $("org-name").focus());
@@ -544,3 +544,143 @@ textarea {
544
544
  #org-create-form {
545
545
  margin-bottom: 12px;
546
546
  }
547
+
548
+ /* ── Product Experience Deepening (v1.6.0) ──────────────────────────────── */
549
+
550
+ /* Workspace summary */
551
+ .summary-card {
552
+ display: flex;
553
+ flex-wrap: wrap;
554
+ justify-content: space-between;
555
+ gap: 16px;
556
+ align-items: center;
557
+ }
558
+ .summary-main { display: flex; align-items: center; gap: 14px; }
559
+ .summary-icon {
560
+ width: 48px; height: 48px; border-radius: 12px;
561
+ display: grid; place-items: center;
562
+ background: #eef2ff; color: var(--blue); font-size: 24px;
563
+ }
564
+ .summary-name { font-weight: 800; font-size: 18px; color: var(--ink); }
565
+ .summary-stats { display: flex; flex-wrap: wrap; gap: 18px; }
566
+ .summary-stat { display: grid; text-align: center; }
567
+ .summary-stat strong { font-size: 20px; color: var(--ink); }
568
+ .summary-stat span { font-size: 11px; color: var(--muted); font-weight: 700; }
569
+ .quickswitch-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 14px; }
570
+ .switch-chip {
571
+ display: inline-flex; align-items: center; gap: 6px;
572
+ border: 1px solid var(--line); background: #fbfcfe; color: var(--ink);
573
+ border-radius: 999px; padding: 6px 12px; font-weight: 700; font-size: 13px; cursor: pointer;
574
+ }
575
+ .switch-chip.active { border-color: var(--blue); background: #eef2ff; color: var(--blue); }
576
+
577
+ /* Entity explorer */
578
+ .entity-card { text-align: left; cursor: pointer; width: 100%; font: inherit; }
579
+ .entity-card.selected { border-color: var(--blue); box-shadow: 0 0 0 2px rgba(37, 99, 235, 0.15); }
580
+ .importance-bar { height: 4px; border-radius: 999px; background: #edf2f7; overflow: hidden; }
581
+ .importance-bar span { display: block; height: 100%; background: linear-gradient(90deg, #2563eb, #7c3aed); }
582
+ .rel-row { display: flex; align-items: center; gap: 8px; font-size: 13px; padding: 2px 0; }
583
+ .rel-dir { color: var(--muted); font-weight: 800; width: 16px; }
584
+ .rel-node { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
585
+ .activity-item .list-title span { font-weight: 700; }
586
+
587
+ /* Skill marketplace tabs */
588
+ .tab-bar { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; }
589
+ .tab {
590
+ border: 1px solid var(--line); background: #fbfcfe; color: var(--muted);
591
+ border-radius: 999px; padding: 6px 14px; font-weight: 700; font-size: 13px; cursor: pointer;
592
+ }
593
+ .tab.active { border-color: var(--blue); background: #eef2ff; color: var(--blue); }
594
+ .tab-count {
595
+ display: inline-block; min-width: 16px; padding: 0 5px; margin-left: 4px;
596
+ border-radius: 999px; background: var(--red); color: #fff; font-size: 11px;
597
+ }
598
+ .tab-count:empty { display: none; }
599
+
600
+ /* Enterprise capability grid */
601
+ .capability-grid {
602
+ display: grid; gap: 10px;
603
+ grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
604
+ margin-top: 12px;
605
+ }
606
+ .capability-card {
607
+ display: flex; align-items: center; gap: 10px;
608
+ border: 1px solid var(--line); border-radius: 8px; padding: 10px 12px; background: #fbfcfe;
609
+ }
610
+ .capability-card i { font-size: 18px; }
611
+ .capability-card.off i { color: var(--muted); }
612
+ .capability-card.on i { color: var(--green); }
613
+ .capability-card .cap-name { flex: 1; font-weight: 700; font-size: 13px; color: var(--ink); }
614
+
615
+ /* Workspace health dashboard + skill install lifecycle (v1.7.0) */
616
+ .health-grid {
617
+ display: grid;
618
+ grid-template-columns: repeat(4, minmax(0, 1fr));
619
+ gap: 12px;
620
+ }
621
+ .health-card {
622
+ min-width: 0;
623
+ border: 1px solid var(--line);
624
+ border-radius: 8px;
625
+ background: #fbfcfe;
626
+ padding: 14px;
627
+ display: grid;
628
+ gap: 7px;
629
+ }
630
+ .health-card i {
631
+ color: var(--blue);
632
+ font-size: 20px;
633
+ }
634
+ .health-card span {
635
+ color: var(--muted);
636
+ font-size: 11px;
637
+ font-weight: 800;
638
+ text-transform: uppercase;
639
+ }
640
+ .health-card strong {
641
+ color: var(--ink);
642
+ font-size: 22px;
643
+ line-height: 1.1;
644
+ overflow-wrap: anywhere;
645
+ }
646
+ .health-card em {
647
+ color: var(--muted);
648
+ font-size: 12px;
649
+ font-style: normal;
650
+ }
651
+ .skill-progress {
652
+ display: grid;
653
+ gap: 6px;
654
+ }
655
+ .skill-progress-head {
656
+ display: flex;
657
+ align-items: center;
658
+ justify-content: space-between;
659
+ color: var(--muted);
660
+ font-size: 11px;
661
+ font-weight: 800;
662
+ }
663
+ .skill-progress-track {
664
+ height: 7px;
665
+ border-radius: 999px;
666
+ background: #e5eaf2;
667
+ overflow: hidden;
668
+ }
669
+ .skill-progress-track span {
670
+ display: block;
671
+ height: 100%;
672
+ border-radius: inherit;
673
+ background: linear-gradient(90deg, var(--blue), var(--green));
674
+ }
675
+
676
+ @media (max-width: 1100px) {
677
+ .health-grid {
678
+ grid-template-columns: repeat(2, minmax(0, 1fr));
679
+ }
680
+ }
681
+
682
+ @media (max-width: 760px) {
683
+ .health-grid {
684
+ grid-template-columns: 1fr;
685
+ }
686
+ }