memorix 1.0.7 → 1.0.9

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.
@@ -96,11 +96,11 @@ const i18n = {
96
96
  noRetentionData: 'No Retention Data',
97
97
  noRetentionDesc: 'Store observations to see memory retention scores',
98
98
 
99
- // Team Collaboration Space
100
- teamTitle: 'Collaboration',
101
- teamSubtitle: 'Project collaboration status who\'s working, what\'s pending',
102
- teamNoData: 'Collaboration requires HTTP transport',
103
- teamNoDataHint: 'Project collaboration (agents, file locks, tasks) requires the HTTP transport. Start it with:',
99
+ // Team -> Autonomous Agent Team
100
+ teamTitle: 'Agent Team',
101
+ teamSubtitle: 'Autonomous CLI agents, tasks, locks, and handoffs',
102
+ teamNoData: 'No autonomous agent workflow activity yet',
103
+ teamNoDataHint: 'Use the CLI to start an autonomous workflow or inspect team state.',
104
104
  teamActiveAgents: 'Active Agents',
105
105
  teamLockedFiles: 'Locked Files',
106
106
  teamTasks: 'Tasks',
@@ -147,6 +147,28 @@ const i18n = {
147
147
  recentGitMemories: 'Recent Git Memories',
148
148
  commit: 'Commit',
149
149
  created: 'Created',
150
+ knowledgeTitle: 'Knowledge Base',
151
+ knowledgeSubtitle: 'Project-scoped engineering knowledge distilled from durable memory',
152
+ knowledgeObservationsUsed: 'Observations Used',
153
+ knowledgeMiniSkillsUsed: 'Mini-skills Used',
154
+ knowledgeRefs: 'Refs',
155
+ knowledgeGenerated: 'Generated',
156
+ knowledgeQuickJump: 'Sections',
157
+ knowledgeNoItems: 'No entries in this section yet',
158
+ knowledgeNoItemsDesc: 'This section will fill in as durable project knowledge is stored.',
159
+ knowledgeUnavailable: 'Knowledge Base Unavailable',
160
+ knowledgeUnavailableDesc: 'Could not load /api/knowledge for the selected project.',
161
+ knowledgeEmpty: 'No Knowledge Base Entries',
162
+ knowledgeEmptyDesc: 'Store durable observations or promote mini-skills to populate this read-only project wiki.',
163
+ knowledgeProvenance: 'Provenance',
164
+ knowledgeEntry: 'entry',
165
+ knowledgeEntries: 'entries',
166
+ knowledgeSectionProjectOverview: 'Project Overview',
167
+ knowledgeSectionCoreDecisions: 'Core Decisions',
168
+ knowledgeSectionOperationalKnowledge: 'Operational Knowledge',
169
+ knowledgeSectionKnownGotchas: 'Known Gotchas',
170
+ knowledgeSectionGitBackedFacts: 'Git-backed Facts',
171
+ knowledgeSectionPromotedSkills: 'Promoted Skills',
150
172
 
151
173
  // Config
152
174
  configTitle: 'Config Provenance',
@@ -205,17 +227,17 @@ const i18n = {
205
227
  projectUnresolved: 'Unresolved',
206
228
  projectUnresolvedDesc: 'No project bound — select a project from the switcher',
207
229
  projectResolved: 'Resolved',
208
- projectScopeProject: 'Project Collaboration',
230
+ projectScopeProject: 'Project Agent Team',
209
231
  projectScopeGlobal: 'All Projects',
210
- projectScopeProjectDesc: 'Current project\'s collaboration space',
211
- projectScopeGlobalDesc: 'All agents across projects',
232
+ projectScopeProjectDesc: 'Autonomous agents and tasks for this project',
233
+ projectScopeGlobalDesc: 'Autonomous agents and tasks across projects',
212
234
 
213
235
  // Team (additional)
214
236
  teamMessages: 'Messages',
215
237
  teamAllRead: 'All read',
216
238
  teamUnread: 'unread',
217
- teamNoAgentsProject: 'No agents in this session',
218
- teamNoAgentsGlobal: 'No agents in any scope',
239
+ teamNoAgentsProject: 'No autonomous agents recorded for this project',
240
+ teamNoAgentsGlobal: 'No autonomous agents recorded in any scope',
219
241
  teamNoFilesLocked: 'No files locked',
220
242
  teamNoTasksCreated: 'No tasks created',
221
243
  teamPending: 'pending',
@@ -235,13 +257,13 @@ const i18n = {
235
257
  teamRecentCount: 'recent',
236
258
  teamHistoricalCount: 'historical',
237
259
  teamHistoricalTotal: 'Historical total',
238
- teamHistoricalHint: 'Inactive for more than 7 days. Not current collaborators.',
260
+ teamHistoricalHint: 'Inactive for more than 7 days. Not current autonomous agents.',
239
261
  teamRecentHint: 'Inactive, last seen within 7 days.',
240
262
  teamShowHistorical: 'Show historical',
241
263
  teamHideHistorical: 'Hide historical',
242
- teamNoActiveNow: 'No active agents right now',
243
- teamNoRecent: 'No agents seen in the last 7 days',
244
- teamSummaryHint: 'Headline shows currently active. Historical rows are collapsed by default.',
264
+ teamNoActiveNow: 'No active autonomous agents right now',
265
+ teamNoRecent: 'No autonomous agents seen in the last 7 days',
266
+ teamSummaryHint: 'Only explicit autonomous agent identities appear here. Historical rows are collapsed by default.',
245
267
  // Resume area
246
268
  resumeTitle: 'Continue This Project',
247
269
  resumeDesc: 'Pick up where you left off',
@@ -314,6 +336,34 @@ const i18n = {
314
336
  graphTopToBottom: 'Top → Bottom',
315
337
  graphMore: 'more',
316
338
 
339
+ // Knowledge Graph (semantic)
340
+ kgTitle: 'Knowledge Graph',
341
+ kgSubtitle: 'Semantic knowledge topology — section clusters, evidence links, provenance',
342
+ kgNodes: 'nodes',
343
+ kgEdges: 'edges',
344
+ kgClusters: 'clusters',
345
+ kgNoData: 'No Knowledge Graph Data',
346
+ kgNoDataDesc: 'Store durable observations or promote mini-skills to generate a semantic knowledge graph.',
347
+ kgClusterFilter: 'Section Cluster',
348
+ kgEdgeTypeFilter: 'Edge Type',
349
+ kgEdgeSupports: 'supports',
350
+ kgEdgeRelatesTo: 'relates to',
351
+ kgEdgeMentions: 'mentions',
352
+ kgEdgeDerivedFrom: 'derived from',
353
+ kgInspectorSummary: 'Summary',
354
+ kgInspectorProvenance: 'Provenance',
355
+ kgInspectorSection: 'Section',
356
+ kgInspectorEntity: 'Entity',
357
+ kgInspectorEvidence: 'Evidence',
358
+ kgInspectorRelatedEdges: 'Related Edges',
359
+ kgInspectorNoEdges: 'No edges',
360
+ kgDataMode: 'Data Source',
361
+ kgModeSemantic: 'Semantic KG',
362
+ kgModeEntity: 'Entity Graph',
363
+ kgViewMode: 'View Mode',
364
+ kgFocused: 'Focused',
365
+ kgFullGraph: 'Full Graph',
366
+
317
367
  // Identity (additional)
318
368
  identityCurrentProject: 'Current Project',
319
369
  identityHistoricalProjects: 'Historical Projects',
@@ -333,25 +383,27 @@ const i18n = {
333
383
  // Nav tooltips + labels
334
384
  navDashboard: 'Dashboard',
335
385
  navGitMemory: 'Git Memory',
386
+ navKnowledge: 'Knowledge Base',
336
387
  navGraph: 'Knowledge Graph',
337
388
  navObservations: 'Observations',
338
389
  navRetention: 'Retention',
339
390
  navConfig: 'Config',
340
391
  navIdentity: 'Identity',
341
392
  navSessions: 'Sessions',
342
- navTeam: 'Collaboration',
393
+ navTeam: 'Agent Team',
343
394
  navLabelDashboard: 'Overview',
344
395
  navLabelGitMemory: 'Git Memory',
396
+ navLabelKnowledge: 'Knowledge',
345
397
  navLabelGraph: 'Graph',
346
398
  navLabelObservations: 'Observations',
347
399
  navLabelRetention: 'Retention',
348
400
  navLabelConfig: 'Config',
349
401
  navLabelIdentity: 'Identity',
350
402
  navLabelSessions: 'Sessions',
351
- navLabelTeam: 'Collaboration',
403
+ navLabelTeam: 'Agent Team',
352
404
  sectionCore: 'CORE',
353
405
  sectionHealth: 'HEALTH',
354
- sectionCollaboration: 'COLLABORATION',
406
+ sectionCollaboration: 'AGENTS',
355
407
  themeDark: 'Dark',
356
408
  themeLight: 'Light',
357
409
  loading: 'Loading...',
@@ -360,8 +412,8 @@ const i18n = {
360
412
  // Mode banner
361
413
  modeStandalone: 'Standalone',
362
414
  modeControlPlane: 'Control Plane',
363
- modeStandaloneHint: 'No live MCP team features unavailable',
364
- modeControlPlaneHint: 'Full MCP + team collaboration',
415
+ modeStandaloneHint: 'Standalone dashboard - memory and read-only agent team state',
416
+ modeControlPlaneHint: 'HTTP control plane - shared MCP access and live dashboard',
365
417
  modeBannerProject: 'Project',
366
418
  modeBannerMcp: 'MCP',
367
419
 
@@ -466,16 +518,16 @@ const i18n = {
466
518
  noRetentionData: '暂无衰减数据',
467
519
  noRetentionDesc: '存储观察记录以查看记忆衰减分数',
468
520
 
469
- // Team 协作空间
470
- teamTitle: '协作',
471
- teamSubtitle: '项目协作状态 谁在工作、什么待处理',
472
- teamNoData: '协作功能需要 HTTP 传输',
473
- teamNoDataHint: '项目协作(Agent 注册、文件锁、任务看板)需要 HTTP 传输模式。使用以下命令启动:',
521
+ // Team -> 自主 Agent 团队
522
+ teamTitle: 'Agent 团队',
523
+ teamSubtitle: '自主 CLI agents、任务、文件锁和交接状态',
524
+ teamNoData: '暂无自主 agent 工作流活动',
525
+ teamNoDataHint: '使用 CLI 启动自主工作流,或查看团队任务状态。',
474
526
  teamActiveAgents: '活跃 Agent',
475
527
  teamLockedFiles: '锁定文件',
476
528
  teamTasks: '任务',
477
529
  teamAvailable: '可领取',
478
- teamAgents: 'Agent 列表',
530
+ teamAgents: 'Agent',
479
531
  teamLocks: '文件锁',
480
532
  teamTaskBoard: '任务看板',
481
533
  // Resume area
@@ -506,17 +558,39 @@ const i18n = {
506
558
 
507
559
  // Git Memory
508
560
  gitMemoryTitle: 'Git 记忆',
509
- gitMemorySubtitle: '来自 git 提交的记忆 — 不可变的事实源',
561
+ gitMemorySubtitle: '来自 git 提交的记忆 — 真实来源且不可变',
510
562
  totalGitMemories: 'Git 记忆总数',
511
- uniqueCommits: '唯一提交数',
563
+ uniqueCommits: '唯一提交',
512
564
  typeCoverage: '类型覆盖',
513
565
  noGitMemory: '暂无 Git 记忆',
514
566
  noGitMemoryDesc: '使用以下命令安装 post-commit hook: memorix git-hook-install',
515
567
  noGitMemoriesYet: '暂无 Git 记忆',
516
568
  noGitMemoriesHint: '安装 post-commit hook 以自动捕获 git 记忆:',
517
- recentGitMemories: '最近的 Git 记忆',
569
+ recentGitMemories: '最近 Git 记忆',
518
570
  commit: '提交',
519
571
  created: '创建时间',
572
+ knowledgeTitle: '知识库',
573
+ knowledgeSubtitle: '从持久项目记忆中提炼出的工程知识',
574
+ knowledgeObservationsUsed: '使用的观察',
575
+ knowledgeMiniSkillsUsed: '使用的 Mini-skills',
576
+ knowledgeRefs: '引用',
577
+ knowledgeGenerated: '生成于',
578
+ knowledgeQuickJump: '章节',
579
+ knowledgeNoItems: '本章节暂无条目',
580
+ knowledgeNoItemsDesc: '当项目中出现持久知识后,本章节会自动填充。',
581
+ knowledgeUnavailable: '知识库不可用',
582
+ knowledgeUnavailableDesc: '无法为所选项目加载 /api/knowledge。',
583
+ knowledgeEmpty: '暂无知识库条目',
584
+ knowledgeEmptyDesc: '存储持久观察或提升 mini-skill 后,此只读项目 wiki 会自动填充。',
585
+ knowledgeProvenance: '来源',
586
+ knowledgeEntry: '条',
587
+ knowledgeEntries: '条',
588
+ knowledgeSectionProjectOverview: '项目概览',
589
+ knowledgeSectionCoreDecisions: '核心决策',
590
+ knowledgeSectionOperationalKnowledge: '操作知识',
591
+ knowledgeSectionKnownGotchas: '已知陷阱',
592
+ knowledgeSectionGitBackedFacts: 'Git 事实',
593
+ knowledgeSectionPromotedSkills: '提升技能',
520
594
 
521
595
  // Config
522
596
  configTitle: '配置溯源',
@@ -575,17 +649,17 @@ const i18n = {
575
649
  projectUnresolved: '未绑定',
576
650
  projectUnresolvedDesc: '无项目绑定 — 请从切换器选择项目',
577
651
  projectResolved: '已绑定',
578
- projectScopeProject: '项目协作',
652
+ projectScopeProject: '项目 Agent 团队',
579
653
  projectScopeGlobal: '所有项目',
580
- projectScopeProjectDesc: '当前项目的协作空间',
581
- projectScopeGlobalDesc: '所有项目中的 Agent',
654
+ projectScopeProjectDesc: '当前项目中的自主 agents 和任务',
655
+ projectScopeGlobalDesc: '所有项目中的自主 agents 和任务',
582
656
 
583
657
  // Team (additional)
584
658
  teamMessages: '消息',
585
659
  teamAllRead: '全部已读',
586
660
  teamUnread: '未读',
587
- teamNoAgentsProject: '当前会话无 Agent',
588
- teamNoAgentsGlobal: '任何范围均无 Agent',
661
+ teamNoAgentsProject: '当前项目暂无自主 agent 记录',
662
+ teamNoAgentsGlobal: '任何范围均无自主 agent 记录',
589
663
  teamNoFilesLocked: '无文件锁定',
590
664
  teamNoTasksCreated: '无已创建任务',
591
665
  teamPending: '待处理',
@@ -605,13 +679,13 @@ const i18n = {
605
679
  teamRecentCount: '近期',
606
680
  teamHistoricalCount: '历史',
607
681
  teamHistoricalTotal: '历史累计',
608
- teamHistoricalHint: '超过 7 天无活动,非当前协作成员',
682
+ teamHistoricalHint: '超过 7 天无活动,非当前自主 agent',
609
683
  teamRecentHint: '未活跃,最近 7 天内有过心跳',
610
684
  teamShowHistorical: '显示历史',
611
685
  teamHideHistorical: '隐藏历史',
612
- teamNoActiveNow: '当前无活跃 Agent',
613
- teamNoRecent: '最近 7 天无 Agent 活动',
614
- teamSummaryHint: '标题显示当前活跃数。历史数据默认折叠。',
686
+ teamNoActiveNow: '当前无活跃自主 agent',
687
+ teamNoRecent: '最近 7 天无自主 agent 活动',
688
+ teamSummaryHint: '这里只显示显式自主 agent 身份。历史数据默认折叠。',
615
689
  // Resume area
616
690
  resumeTitle: '继续此项目',
617
691
  resumeDesc: '从上次中断处继续',
@@ -689,6 +763,34 @@ const i18n = {
689
763
  graphTopToBottom: '从上到下',
690
764
  graphMore: '更多',
691
765
 
766
+ // Knowledge Graph (semantic)
767
+ kgTitle: '知识图谱',
768
+ kgSubtitle: '语义知识拓扑 — 分区聚类、证据关联、溯源',
769
+ kgNodes: '个节点',
770
+ kgEdges: '条边',
771
+ kgClusters: '个聚类',
772
+ kgNoData: '暂无知识图谱数据',
773
+ kgNoDataDesc: '存储持久观察或提升迷你技能以生成语义知识图谱。',
774
+ kgClusterFilter: '分区聚类',
775
+ kgEdgeTypeFilter: '边类型',
776
+ kgEdgeSupports: '支撑',
777
+ kgEdgeRelatesTo: '关联',
778
+ kgEdgeMentions: '提及',
779
+ kgEdgeDerivedFrom: '派生自',
780
+ kgInspectorSummary: '摘要',
781
+ kgInspectorProvenance: '溯源',
782
+ kgInspectorSection: '分区',
783
+ kgInspectorEntity: '实体',
784
+ kgInspectorEvidence: '证据',
785
+ kgInspectorRelatedEdges: '相关边',
786
+ kgInspectorNoEdges: '无边',
787
+ kgDataMode: '数据源',
788
+ kgModeSemantic: '语义图谱',
789
+ kgModeEntity: '实体图谱',
790
+ kgViewMode: '视图模式',
791
+ kgFocused: '聚焦',
792
+ kgFullGraph: '全量',
793
+
692
794
  // Identity (additional)
693
795
  identityCurrentProject: '当前项目',
694
796
  identityHistoricalProjects: '历史项目',
@@ -708,25 +810,27 @@ const i18n = {
708
810
  // Nav tooltips
709
811
  navDashboard: '仪表盘',
710
812
  navGitMemory: 'Git 记忆',
813
+ navKnowledge: '知识库',
711
814
  navGraph: '知识图谱',
712
815
  navObservations: '观察记录',
713
816
  navRetention: '记忆衰减',
714
817
  navConfig: '配置溯源',
715
818
  navIdentity: '身份健康',
716
819
  navSessions: '会话',
717
- navTeam: '协作',
820
+ navTeam: 'Agent 团队',
718
821
  navLabelDashboard: '概览',
719
822
  navLabelGitMemory: 'Git 记忆',
823
+ navLabelKnowledge: '知识库',
720
824
  navLabelGraph: '图谱',
721
825
  navLabelObservations: '观察',
722
826
  navLabelRetention: '衰减',
723
827
  navLabelConfig: '配置',
724
828
  navLabelIdentity: '身份',
725
829
  navLabelSessions: '会话',
726
- navLabelTeam: '协作',
830
+ navLabelTeam: 'Agent 团队',
727
831
  sectionCore: '核心',
728
832
  sectionHealth: '健康',
729
- sectionCollaboration: '协作',
833
+ sectionCollaboration: 'Agents',
730
834
  themeDark: '深色',
731
835
  themeLight: '浅色',
732
836
  loading: '加载中...',
@@ -735,8 +839,8 @@ const i18n = {
735
839
  // Mode banner
736
840
  modeStandalone: '独立模式',
737
841
  modeControlPlane: '控制平面',
738
- modeStandaloneHint: '无实时 MCP 团队功能不可用',
739
- modeControlPlaneHint: '完整 MCP + 团队协作',
842
+ modeStandaloneHint: '独立看板 - 记忆与只读 Agent 团队状态',
843
+ modeControlPlaneHint: 'HTTP 控制平面 - 共享 MCP 接入与实时看板',
740
844
  modeBannerProject: '项目',
741
845
  modeBannerMcp: 'MCP',
742
846
 
@@ -764,6 +868,14 @@ function t(key) {
764
868
  return (i18n[currentLang] && i18n[currentLang][key]) || i18n.en[key] || key;
765
869
  }
766
870
 
871
+ function onDomReady(fn) {
872
+ if (document.readyState === 'loading') {
873
+ document.addEventListener('DOMContentLoaded', fn, { once: true });
874
+ } else {
875
+ fn();
876
+ }
877
+ }
878
+
767
879
  function setLang(lang) {
768
880
  currentLang = lang;
769
881
  localStorage.setItem('memorix-lang', lang);
@@ -774,8 +886,8 @@ function setLang(lang) {
774
886
  if (label) label.textContent = lang === 'en' ? '中文' : 'EN';
775
887
 
776
888
  // Update nav tooltips + labels
777
- const tooltipMap = { dashboard: 'navDashboard', 'git-memory': 'navGitMemory', graph: 'navGraph', observations: 'navObservations', retention: 'navRetention', config: 'navConfig', identity: 'navIdentity', sessions: 'navSessions', team: 'navTeam' };
778
- const labelMap = { dashboard: 'navLabelDashboard', 'git-memory': 'navLabelGitMemory', graph: 'navLabelGraph', observations: 'navLabelObservations', retention: 'navLabelRetention', config: 'navLabelConfig', identity: 'navLabelIdentity', sessions: 'navLabelSessions', team: 'navLabelTeam' };
889
+ const tooltipMap = { dashboard: 'navDashboard', 'git-memory': 'navGitMemory', knowledge: 'navKnowledge', graph: 'navGraph', observations: 'navObservations', retention: 'navRetention', config: 'navConfig', identity: 'navIdentity', sessions: 'navSessions', team: 'navTeam' };
890
+ const labelMap = { dashboard: 'navLabelDashboard', 'git-memory': 'navLabelGitMemory', knowledge: 'navLabelKnowledge', graph: 'navLabelGraph', observations: 'navLabelObservations', retention: 'navLabelRetention', config: 'navLabelConfig', identity: 'navLabelIdentity', sessions: 'navLabelSessions', team: 'navLabelTeam' };
779
891
  document.querySelectorAll('.nav-btn').forEach(b => {
780
892
  const page = b.dataset.page;
781
893
  if (page && tooltipMap[page]) b.title = t(tooltipMap[page]);
@@ -810,7 +922,7 @@ function setLang(lang) {
810
922
  }
811
923
 
812
924
  // Init lang toggle button
813
- document.addEventListener('DOMContentLoaded', () => {
925
+ onDomReady(() => {
814
926
  const btn = document.getElementById('lang-toggle');
815
927
  const label = document.getElementById('lang-label');
816
928
  if (label) label.textContent = currentLang === 'en' ? '中文' : 'EN';
@@ -853,7 +965,7 @@ function applyTheme(theme) {
853
965
  // Apply saved theme immediately
854
966
  applyTheme(currentTheme);
855
967
 
856
- document.addEventListener('DOMContentLoaded', () => {
968
+ onDomReady(() => {
857
969
  const themeBtn = document.getElementById('theme-toggle');
858
970
  if (themeBtn) {
859
971
  themeBtn.addEventListener('click', () => {
@@ -866,7 +978,7 @@ document.addEventListener('DOMContentLoaded', () => {
866
978
  // Router & Navigation
867
979
  // ============================================================
868
980
 
869
- const pages = ['dashboard', 'git-memory', 'graph', 'observations', 'retention', 'config', 'identity', 'sessions', 'team'];
981
+ const pages = ['dashboard', 'git-memory', 'knowledge', 'graph', 'observations', 'retention', 'config', 'identity', 'sessions', 'team'];
870
982
  let currentPage = 'dashboard';
871
983
 
872
984
  function navigate(page) {
@@ -1151,7 +1263,7 @@ async function initProjectSwitcher() {
1151
1263
  }
1152
1264
  }
1153
1265
 
1154
- document.addEventListener('DOMContentLoaded', () => {
1266
+ onDomReady(() => {
1155
1267
  initProjectSwitcher();
1156
1268
  });
1157
1269
 
@@ -1167,6 +1279,7 @@ async function loadPage(page) {
1167
1279
  switch (page) {
1168
1280
  case 'dashboard': await loadDashboard(); break;
1169
1281
  case 'git-memory': await loadGitMemory(); break;
1282
+ case 'knowledge': await loadKnowledge(); break;
1170
1283
  case 'graph': await loadGraph(); break;
1171
1284
  case 'observations': await loadObservations(); break;
1172
1285
  case 'retention': await loadRetention(); break;
@@ -1332,7 +1445,7 @@ async function loadDashboard() {
1332
1445
  <div style="flex: 1;">
1333
1446
  ${typeEntries.map(([type, count]) => `
1334
1447
  <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px;">
1335
- <span style="width: 18px; text-align: center; font-size: 13px;">${typeIcons[type] || ''}</span>
1448
+ <span style="width: 18px; text-align: center; font-size: 13px;">${typeIcons[type] || '[UNKNOWN]'}</span>
1336
1449
  <span style="width: 110px; font-size: 11px; color: var(--text-secondary);">${type}</span>
1337
1450
  <div style="flex: 1; height: 5px; background: rgba(128,128,128,0.1); border-radius: 3px; overflow: hidden;">
1338
1451
  <div style="width: ${(count / maxTypeCount) * 100}%; height: 100%; background: var(--type-${type}, var(--accent-cyan)); border-radius: 3px;"></div>
@@ -1417,6 +1530,201 @@ function renderPieChart(canvasId, entries, icons) {
1417
1530
  ctx.fillText('total', cx, cy + 10);
1418
1531
  }
1419
1532
 
1533
+ async function loadKnowledge() {
1534
+ const container = document.getElementById('page-knowledge');
1535
+ container.innerHTML = '<div class="loading knowledge-loading"><div class="spinner"></div><div class="knowledge-loading-text">' + t('loading') + '</div></div>';
1536
+
1537
+ const kb = await api('knowledge');
1538
+ if (!kb) {
1539
+ container.innerHTML = `
1540
+ <div class="page-header">
1541
+ <h1 class="page-title">${t('knowledgeTitle')}</h1>
1542
+ <p class="page-subtitle">${t('knowledgeSubtitle')}</p>
1543
+ </div>
1544
+ <div class="panel knowledge-error-panel">
1545
+ <div class="panel-body">
1546
+ <div class="knowledge-state-icon"><span class="iconify" data-icon="lucide:book-x"></span></div>
1547
+ <div class="empty-state-title">${t('knowledgeUnavailable')}</div>
1548
+ <div class="empty-state-desc">${t('knowledgeUnavailableDesc')}</div>
1549
+ <button class="team-refresh-btn" id="knowledge-retry-btn" style="margin:16px auto 0;">${t('teamRefresh')}</button>
1550
+ </div>
1551
+ </div>
1552
+ `;
1553
+ const retry = document.getElementById('knowledge-retry-btn');
1554
+ if (retry) retry.addEventListener('click', () => {
1555
+ delete loaded.knowledge;
1556
+ loadPage('knowledge');
1557
+ });
1558
+ return;
1559
+ }
1560
+
1561
+ const sections = normalizeKnowledgeSections(kb.sections || []);
1562
+ const totalItems = sections.reduce((sum, section) => sum + (section.items || []).length, 0);
1563
+ const stats = kb.stats || { observationsUsed: 0, miniSkillsUsed: 0, refs: 0 };
1564
+ const overviewSection = sections.find(s => s.id === 'project-overview');
1565
+ const mainSections = sections.filter(s => s.id !== 'project-overview');
1566
+
1567
+ container.innerHTML = `
1568
+ <div class="knowledge-page-shell">
1569
+ <div class="knowledge-topbar">
1570
+ <div class="page-header knowledge-header">
1571
+ <div class="knowledge-title-block">
1572
+ <h1 class="page-title">${escapeHtml(t('knowledgeTitle'))}</h1>
1573
+ <p class="page-subtitle">${t('knowledgeSubtitle')}</p>
1574
+ <div class="knowledge-project-line" title="${escapeHtml(kb.projectId || '')}">${escapeHtml(kb.projectId || '')}</div>
1575
+ </div>
1576
+ <div class="knowledge-generated">
1577
+ <span>${t('knowledgeGenerated')}</span>
1578
+ <strong>${escapeHtml(formatTime(kb.generatedAt))}</strong>
1579
+ </div>
1580
+ </div>
1581
+
1582
+ <div class="knowledge-stats-grid">
1583
+ <div class="stat-card" data-accent="purple">
1584
+ <div class="stat-label">${t('knowledgeObservationsUsed')}</div>
1585
+ <div class="stat-value">${stats.observationsUsed || 0}</div>
1586
+ </div>
1587
+ <div class="stat-card" data-accent="cyan">
1588
+ <div class="stat-label">${t('knowledgeMiniSkillsUsed')}</div>
1589
+ <div class="stat-value">${stats.miniSkillsUsed || 0}</div>
1590
+ </div>
1591
+ <div class="stat-card" data-accent="green">
1592
+ <div class="stat-label">${t('knowledgeRefs')}</div>
1593
+ <div class="stat-value">${stats.refs || 0}</div>
1594
+ </div>
1595
+ </div>
1596
+
1597
+ <nav class="knowledge-jump" aria-label="${t('knowledgeQuickJump')}">
1598
+ <span class="knowledge-jump-label">${t('knowledgeQuickJump')}</span>
1599
+ ${sections.map(section => `<a href="#${knowledgeAnchor(section.id)}" class="knowledge-jump-chip">${escapeHtml(section.title)} <span>${(section.items || []).length}</span></a>`).join('')}
1600
+ </nav>
1601
+
1602
+ ${overviewSection ? renderKnowledgeOverviewCard(overviewSection) : ''}
1603
+ </div>
1604
+
1605
+ ${totalItems === 0 ? `
1606
+ <div class="panel knowledge-empty-overview">
1607
+ <div class="panel-body">
1608
+ <div class="knowledge-state-icon"><span class="iconify" data-icon="lucide:book-open"></span></div>
1609
+ <div class="empty-state-title">${t('knowledgeEmpty')}</div>
1610
+ <div class="empty-state-desc">${t('knowledgeEmptyDesc')}</div>
1611
+ </div>
1612
+ </div>
1613
+ ` : `
1614
+ <div class="knowledge-sections-region">
1615
+ ${mainSections.map(renderKnowledgeSection).join('')}
1616
+ </div>
1617
+ `}
1618
+ </div>
1619
+ `;
1620
+ }
1621
+
1622
+ function normalizeKnowledgeSections(sections) {
1623
+ const byId = new Map(sections.map(section => [section.id, section]));
1624
+ const order = [
1625
+ ['project-overview', 'knowledgeSectionProjectOverview'],
1626
+ ['core-decisions', 'knowledgeSectionCoreDecisions'],
1627
+ ['operational-knowledge', 'knowledgeSectionOperationalKnowledge'],
1628
+ ['known-gotchas', 'knowledgeSectionKnownGotchas'],
1629
+ ['git-backed-facts', 'knowledgeSectionGitBackedFacts'],
1630
+ ['promoted-skills', 'knowledgeSectionPromotedSkills'],
1631
+ ];
1632
+ return order.map(([id, titleKey]) => {
1633
+ const section = byId.get(id);
1634
+ const localizedTitle = t(titleKey);
1635
+ if (section) return { ...section, title: localizedTitle };
1636
+ return { id, title: localizedTitle, items: [], empty: true };
1637
+ });
1638
+ }
1639
+
1640
+ function knowledgeEntryLabel(count) {
1641
+ return count === 1 ? t('knowledgeEntry') : t('knowledgeEntries');
1642
+ }
1643
+
1644
+ function renderKnowledgeOverviewCard(section) {
1645
+ const item = (section.items || [])[0];
1646
+ if (!item) {
1647
+ return `
1648
+ <section class="knowledge-overview-card knowledge-overview-card--empty" id="${knowledgeAnchor(section.id)}">
1649
+ <div class="knowledge-overview-meta">
1650
+ <span class="knowledge-overview-label">${escapeHtml(section.title)}</span>
1651
+ <span class="knowledge-section-meta">0 ${t('knowledgeEntries')}</span>
1652
+ </div>
1653
+ <div class="knowledge-overview-body">
1654
+ <p class="knowledge-summary">${t('knowledgeNoItemsDesc')}</p>
1655
+ </div>
1656
+ </section>
1657
+ `;
1658
+ }
1659
+ return `
1660
+ <section class="knowledge-overview-card" id="${knowledgeAnchor(section.id)}">
1661
+ <div class="knowledge-overview-meta">
1662
+ <span class="knowledge-overview-label">${escapeHtml(section.title)}</span>
1663
+ <span class="knowledge-overview-title" title="${escapeHtml(item.title || '')}">${escapeHtml(item.title || t('untitled'))}</span>
1664
+ </div>
1665
+ <div class="knowledge-overview-body">
1666
+ <p class="knowledge-summary knowledge-summary--overview">${escapeHtml(item.summary || '')}</p>
1667
+ <div class="knowledge-ref-list">${renderKnowledgeRefs(item.refs || [])}</div>
1668
+ </div>
1669
+ </section>
1670
+ `;
1671
+ }
1672
+
1673
+ function renderKnowledgeSection(section) {
1674
+ const items = section.items || [];
1675
+ return `
1676
+ <section class="panel knowledge-section" id="${knowledgeAnchor(section.id)}">
1677
+ <div class="panel-header knowledge-section-header">
1678
+ <span class="panel-title">${escapeHtml(section.title)}</span>
1679
+ <span class="knowledge-section-meta">${items.length} ${knowledgeEntryLabel(items.length)}</span>
1680
+ </div>
1681
+ <div class="panel-body knowledge-section-body">
1682
+ ${items.length === 0 ? renderKnowledgeEmptySection() : items.map(renderKnowledgeItem).join('')}
1683
+ </div>
1684
+ </section>
1685
+ `;
1686
+ }
1687
+
1688
+ function renderKnowledgeItem(item) {
1689
+ const refs = item.refs || [];
1690
+ return `
1691
+ <article class="knowledge-item">
1692
+ <div class="knowledge-item-head">
1693
+ <h3 title="${escapeHtml(item.title || t('untitled'))}">${escapeHtml(item.title || t('untitled'))}</h3>
1694
+ <span class="type-badge" data-type="${escapeHtml(item.type || 'unknown')}">${escapeHtml(item.type || t('unknown'))}</span>
1695
+ </div>
1696
+ ${item.entityName ? `<div class="knowledge-entity" title="${escapeHtml(item.entityName)}">${escapeHtml(item.entityName)}</div>` : ''}
1697
+ <p class="knowledge-summary">${escapeHtml(item.summary || '')}</p>
1698
+ ${refs.length > 0 ? `<div class="knowledge-ref-list">${renderKnowledgeRefs(refs)}</div>` : ''}
1699
+ </article>
1700
+ `;
1701
+ }
1702
+
1703
+ function renderKnowledgeRefs(refs) {
1704
+ if (refs.length === 0) return '<span class="knowledge-ref-empty">—</span>';
1705
+ return refs.map(ref => {
1706
+ const kind = ref.kind || 'observation';
1707
+ const title = ref.title ? ` title="${escapeHtml(ref.title)}"` : '';
1708
+ return `<span class="knowledge-ref-chip" data-kind="${escapeHtml(kind)}"${title}>${escapeHtml(ref.id || kind)}</span>`;
1709
+ }).join('');
1710
+ }
1711
+
1712
+ function renderKnowledgeEmptySection() {
1713
+ return `
1714
+ <div class="knowledge-section-empty">
1715
+ <span class="iconify" data-icon="lucide:circle-dashed"></span>
1716
+ <div>
1717
+ <strong>${t('knowledgeNoItems')}</strong>
1718
+ <span>${t('knowledgeNoItemsDesc')}</span>
1719
+ </div>
1720
+ </div>
1721
+ `;
1722
+ }
1723
+
1724
+ function knowledgeAnchor(id) {
1725
+ return `knowledge-${String(id || 'section').replace(/[^a-z0-9_-]/gi, '-')}`;
1726
+ }
1727
+
1420
1728
  // ============================================================
1421
1729
  // Memory Topology Explorer — Cytoscape.js + Dagre
1422
1730
  // Focused topology default, not full graph dump
@@ -1428,9 +1736,21 @@ async function loadGraph() {
1428
1736
  const container = document.getElementById('page-graph');
1429
1737
  container.innerHTML = '<div class="loading"><div class="spinner"></div></div>';
1430
1738
 
1739
+ // Try semantic Knowledge Graph first
1740
+ try {
1741
+ const kg = await api('knowledge-graph');
1742
+ if (kg && kg.nodes && kg.nodes.length > 0) {
1743
+ renderSemanticGraph(kg);
1744
+ return;
1745
+ }
1746
+ } catch (_e) {
1747
+ // Fallback to entity graph
1748
+ }
1749
+
1750
+ // Fallback: entity/relation graph
1431
1751
  const graph = await api('graph');
1432
1752
  if (!graph || (graph.entities.length === 0 && graph.relations.length === 0)) {
1433
- container.innerHTML = emptyState('<span class="iconify" data-icon="lucide:network" style="font-size:36px;"></span>', t('noGraphData'), t('noGraphDataDesc'));
1753
+ container.innerHTML = emptyState('<span class="iconify" data-icon="lucide:network" style="font-size:36px;"></span>', t('kgNoData'), t('kgNoDataDesc'));
1434
1754
  return;
1435
1755
  }
1436
1756
 
@@ -1482,6 +1802,610 @@ async function loadGraph() {
1482
1802
  renderGraph(graph);
1483
1803
  }
1484
1804
 
1805
+ // ============================================================
1806
+ // Semantic Knowledge Graph Renderer — Apache ECharts 5
1807
+ // Force layout, section categories, evidence-based edges
1808
+ // ============================================================
1809
+
1810
+ function renderSemanticGraph(kg) {
1811
+ const container = document.getElementById('page-graph');
1812
+
1813
+ // Section color palette (vivid for both light/dark themes)
1814
+ const sectionPalette = {
1815
+ 'core-decisions': '#7B9FD9',
1816
+ 'operational-knowledge': '#7CC598',
1817
+ 'known-gotchas': '#E08585',
1818
+ 'git-backed-facts': '#56D6A6',
1819
+ 'promoted-skills': '#C9A8FF',
1820
+ };
1821
+ const defaultSectionColor = '#A893C2';
1822
+
1823
+ function getSectionColor(sectionId) {
1824
+ return sectionPalette[sectionId] || defaultSectionColor;
1825
+ }
1826
+
1827
+ // Edge type styles — solid hex + opacity controlled by G6 strokeOpacity
1828
+ const edgeStyleMap = {
1829
+ 'supports': { color: '#69F0AE', arrow: true, dash: false, label: t('kgEdgeSupports') },
1830
+ 'relates_to': { color: '#80D8FF', arrow: false, dash: [4, 4], label: t('kgEdgeRelatesTo') },
1831
+ 'mentions': { color: '#D0BCFF', arrow: true, dash: false, label: t('kgEdgeMentions') },
1832
+ 'derived_from': { color: '#FFB74D', arrow: true, dash: [6, 3], label: t('kgEdgeDerivedFrom') },
1833
+ };
1834
+
1835
+ // Section label i18n map (matches knowledgeSection* keys)
1836
+ const sectionI18nKeys = {
1837
+ 'core-decisions': 'knowledgeSectionCoreDecisions',
1838
+ 'operational-knowledge': 'knowledgeSectionOperationalKnowledge',
1839
+ 'known-gotchas': 'knowledgeSectionKnownGotchas',
1840
+ 'git-backed-facts': 'knowledgeSectionGitBackedFacts',
1841
+ 'promoted-skills': 'knowledgeSectionPromotedSkills',
1842
+ };
1843
+ function sectionLabel(sectionId) {
1844
+ const key = sectionI18nKeys[sectionId];
1845
+ return key ? t(key) : sectionId;
1846
+ }
1847
+
1848
+ // State
1849
+ let activeSections = new Set((kg.clusters || []).map(c => c.sectionId));
1850
+ let activeEdgeTypes = new Set(Object.keys(edgeStyleMap).filter(type => type !== 'relates_to'));
1851
+ let selectedNodeId = null;
1852
+ let echartsInstance = null;
1853
+ let echartsResizeObserver = null;
1854
+ let focusedMode = true; // default: show top-N nodes only
1855
+ const FOCUSED_TOP_N = 40; // max nodes in focused view
1856
+ const MAX_EDGES_PER_NODE_FOCUSED = 4; // cap per-node edges (top-K by priority) to keep graph readable
1857
+ const FOCUSED_EDGE_BUDGET = 120; // hard visual budget for first-render readability
1858
+
1859
+ function isLight() { return document.documentElement.getAttribute('data-theme') === 'light'; }
1860
+
1861
+ // Compute degree (edge count) for each node
1862
+ const nodeDegreeMap = new Map();
1863
+ for (const edge of kg.edges) {
1864
+ nodeDegreeMap.set(edge.source, (nodeDegreeMap.get(edge.source) || 0) + 1);
1865
+ nodeDegreeMap.set(edge.target, (nodeDegreeMap.get(edge.target) || 0) + 1);
1866
+ }
1867
+
1868
+ // Section ordering for stable category index
1869
+ const SECTION_ORDER = ['core-decisions', 'operational-knowledge', 'known-gotchas', 'git-backed-facts', 'promoted-skills'];
1870
+
1871
+ // Build ECharts graph data: { categories, nodes, links }
1872
+ function buildEChartsData() {
1873
+ // Determine which nodes to show
1874
+ let visibleNodes = kg.nodes.filter(n => activeSections.has(n.sectionId));
1875
+ if (focusedMode && visibleNodes.length > FOCUSED_TOP_N) {
1876
+ visibleNodes.sort((a, b) => {
1877
+ const scoreA = (a.evidenceCount || 0) + (nodeDegreeMap.get(a.id) || 0) * 2;
1878
+ const scoreB = (b.evidenceCount || 0) + (nodeDegreeMap.get(b.id) || 0) * 2;
1879
+ return scoreB - scoreA;
1880
+ });
1881
+ visibleNodes = visibleNodes.slice(0, FOCUSED_TOP_N);
1882
+ }
1883
+ const visibleNodeIds = new Set(visibleNodes.map(n => n.id));
1884
+
1885
+ // Categories (one per section, ECharts uses index reference from nodes)
1886
+ const categories = SECTION_ORDER.map(sectionId => ({
1887
+ name: sectionLabel(sectionId),
1888
+ sectionId,
1889
+ itemStyle: { color: getSectionColor(sectionId) },
1890
+ }));
1891
+ const categoryIndexBySection = Object.fromEntries(SECTION_ORDER.map((s, i) => [s, i]));
1892
+ const visibleBySection = new Map(SECTION_ORDER.map(sectionId => [sectionId, []]));
1893
+ visibleNodes.forEach(node => {
1894
+ const group = visibleBySection.get(node.sectionId) || [];
1895
+ group.push(node);
1896
+ visibleBySection.set(node.sectionId, group);
1897
+ });
1898
+
1899
+ const sectionCenters = {
1900
+ 'core-decisions': { x: -360, y: -170 },
1901
+ 'operational-knowledge': { x: 0, y: 0 },
1902
+ 'known-gotchas': { x: -340, y: 190 },
1903
+ 'git-backed-facts': { x: 360, y: -150 },
1904
+ 'promoted-skills': { x: 350, y: 190 },
1905
+ };
1906
+
1907
+ function stableNodePosition(node, index) {
1908
+ const group = visibleBySection.get(node.sectionId) || [];
1909
+ const total = Math.max(1, group.length);
1910
+ const center = sectionCenters[node.sectionId] || { x: 0, y: 0 };
1911
+ const ring = Math.floor(index / 10);
1912
+ const ringIndex = index % 10;
1913
+ const radius = total === 1 ? 0 : 34 + ring * 42;
1914
+ const angleStep = (Math.PI * 2) / Math.min(10, total);
1915
+ const angleOffset = SECTION_ORDER.indexOf(node.sectionId) * 0.55;
1916
+ const angle = ringIndex * angleStep + angleOffset;
1917
+ return {
1918
+ x: Math.round(center.x + Math.cos(angle) * radius),
1919
+ y: Math.round(center.y + Math.sin(angle) * radius),
1920
+ };
1921
+ }
1922
+
1923
+ // Nodes (stable coordinates; dragging a node must not trigger global force rotation)
1924
+ const nodes = visibleNodes.map(node => {
1925
+ const symbolSize = Math.max(14, Math.min(10 + Math.sqrt(node.evidenceCount || 1) * 4, 32));
1926
+ const sectionIndex = (visibleBySection.get(node.sectionId) || []).findIndex(n => n.id === node.id);
1927
+ const pos = stableNodePosition(node, Math.max(0, sectionIndex));
1928
+ return {
1929
+ id: node.id,
1930
+ name: node.label.length > 28 ? node.label.slice(0, 26) + '\u2026' : node.label,
1931
+ x: pos.x,
1932
+ y: pos.y,
1933
+ category: categoryIndexBySection[node.sectionId] ?? 0,
1934
+ symbolSize,
1935
+ value: node.evidenceCount || 0,
1936
+ sectionId: node.sectionId,
1937
+ nodeType: node.nodeType,
1938
+ entityName: node.entityName || '',
1939
+ evidenceCount: node.evidenceCount || 0,
1940
+ summary: node.summary || '',
1941
+ refs: node.refs || [],
1942
+ fullLabel: node.label,
1943
+ };
1944
+ });
1945
+
1946
+ // Collect candidate edges (both endpoints visible + active type)
1947
+ const edgePriority = { supports: 3, derived_from: 2, mentions: 1, relates_to: 0 };
1948
+ const candidateEdges = kg.edges.filter(e =>
1949
+ activeEdgeTypes.has(e.edgeType) &&
1950
+ visibleNodeIds.has(e.source) &&
1951
+ visibleNodeIds.has(e.target),
1952
+ );
1953
+
1954
+ // In focused mode, cap each node's edges to top-K strongest by type priority
1955
+ let finalEdges = candidateEdges;
1956
+ if (focusedMode && candidateEdges.length > 0) {
1957
+ // Sort all candidates by priority descending (stronger types first)
1958
+ const sorted = [...candidateEdges].sort(
1959
+ (a, b) => (edgePriority[b.edgeType] ?? 0) - (edgePriority[a.edgeType] ?? 0),
1960
+ );
1961
+ const perNodeCount = new Map();
1962
+ finalEdges = [];
1963
+ for (const e of sorted) {
1964
+ if (finalEdges.length >= FOCUSED_EDGE_BUDGET) break;
1965
+ const sCount = perNodeCount.get(e.source) || 0;
1966
+ const tCount = perNodeCount.get(e.target) || 0;
1967
+ if (sCount >= MAX_EDGES_PER_NODE_FOCUSED || tCount >= MAX_EDGES_PER_NODE_FOCUSED) continue;
1968
+ perNodeCount.set(e.source, sCount + 1);
1969
+ perNodeCount.set(e.target, tCount + 1);
1970
+ finalEdges.push(e);
1971
+ }
1972
+ }
1973
+
1974
+ // Build ECharts link objects
1975
+ const links = finalEdges.map(edge => {
1976
+ const style = edgeStyleMap[edge.edgeType] || {};
1977
+ return {
1978
+ id: edge.id,
1979
+ source: edge.source,
1980
+ target: edge.target,
1981
+ edgeType: edge.edgeType,
1982
+ edgeLabel: style.label || edge.edgeType,
1983
+ lineStyle: {
1984
+ color: style.color || '#999',
1985
+ opacity: 0.5,
1986
+ width: 1.1,
1987
+ curveness: 0.12,
1988
+ type: Array.isArray(style.dash) ? 'dashed' : 'solid',
1989
+ },
1990
+ symbol: style.arrow ? ['none', 'arrow'] : ['none', 'none'],
1991
+ symbolSize: [4, 7],
1992
+ };
1993
+ });
1994
+
1995
+ return { categories, nodes, links };
1996
+ }
1997
+
1998
+ let lastRenderedCounts = { nodes: 0, edges: 0 };
1999
+
2000
+ // Render the page shell
2001
+ container.innerHTML = `
2002
+ <div class="kg-page-shell">
2003
+ <div class="page-header">
2004
+ <h1 class="page-title">${t('kgTitle')}</h1>
2005
+ <p class="page-subtitle">${t('kgSubtitle')}</p>
2006
+ </div>
2007
+ <div class="graph-layout">
2008
+ <div class="graph-filter-panel" id="kg-filter-panel"></div>
2009
+ <div id="graph-container">
2010
+ <div id="echarts-mount" style="width:100%;height:100%;"></div>
2011
+ <div class="graph-status-bar">
2012
+ <span class="graph-status-item" id="gs-nodes"></span>
2013
+ <span class="graph-status-item" id="gs-edges"></span>
2014
+ <span class="graph-status-item" id="gs-clusters"></span>
2015
+ <div class="graph-zoom-controls">
2016
+ <button class="graph-zoom-btn" id="gz-out">\u2212</button>
2017
+ <button class="graph-zoom-btn" id="gz-fit">\u2B21</button>
2018
+ <button class="graph-zoom-btn" id="gz-in">+</button>
2019
+ </div>
2020
+ </div>
2021
+ </div>
2022
+ <div class="graph-inspector" id="graph-inspector">
2023
+ <div class="gi-empty"><div class="gi-empty-icon">\u2B21</div>${t('graphSelectNode')}</div>
2024
+ </div>
2025
+ </div>
2026
+ </div>
2027
+ `;
2028
+
2029
+ function initEChartsGraph() {
2030
+ const mountEl = document.getElementById('echarts-mount');
2031
+ if (!mountEl || typeof echarts === 'undefined') return;
2032
+
2033
+ // Ensure container has dimensions before init
2034
+ if (mountEl.clientWidth === 0 || mountEl.clientHeight === 0) {
2035
+ // Defer until layout finishes
2036
+ requestAnimationFrame(() => initEChartsGraph());
2037
+ return;
2038
+ }
2039
+
2040
+ // Dispose previous instance if any
2041
+ if (echartsInstance) {
2042
+ try { echartsInstance.dispose(); } catch (_) { /* noop */ }
2043
+ echartsInstance = null;
2044
+ }
2045
+
2046
+ const data = buildEChartsData();
2047
+ lastRenderedCounts = { nodes: data.nodes.length, edges: data.links.length };
2048
+ const light = isLight();
2049
+ const labelColor = light ? '#1C1B1F' : '#E6E1E5';
2050
+ const labelMutedColor = light ? '#666' : '#999';
2051
+ const tooltipBg = light ? 'rgba(255,255,255,0.96)' : 'rgba(20,20,28,0.96)';
2052
+ const tooltipBorder = light ? 'rgba(0,0,0,0.1)' : 'rgba(255,255,255,0.12)';
2053
+
2054
+ echartsInstance = echarts.init(mountEl, null, { renderer: 'canvas' });
2055
+
2056
+ const wrap = (txt) => String(txt ?? '').replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
2057
+
2058
+ const option = {
2059
+ backgroundColor: 'transparent',
2060
+ tooltip: {
2061
+ trigger: 'item',
2062
+ backgroundColor: tooltipBg,
2063
+ borderColor: tooltipBorder,
2064
+ borderWidth: 1,
2065
+ textStyle: { color: labelColor, fontSize: 12, fontFamily: 'Inter, system-ui, sans-serif' },
2066
+ extraCssText: 'box-shadow: 0 4px 16px rgba(0,0,0,0.18); border-radius: 8px; padding: 10px 12px; max-width: 320px;',
2067
+ formatter: (params) => {
2068
+ if (params.dataType === 'node') {
2069
+ const d = params.data;
2070
+ const summary = d.summary ? String(d.summary).slice(0, 160) : '';
2071
+ return (
2072
+ `<div style="font-weight:600;font-size:13px;margin-bottom:4px;line-height:1.35;">${wrap(d.fullLabel || d.name)}</div>` +
2073
+ `<div style="font-size:11px;color:${labelMutedColor};margin-bottom:6px;">${wrap(d.nodeType || '')}${d.entityName ? ' \u00b7 ' + wrap(d.entityName) : ''}</div>` +
2074
+ `<div style="font-size:11px;color:${labelMutedColor};">${d.evidenceCount || 0} ${wrap(t('kgInspectorEvidence'))}</div>` +
2075
+ (summary ? `<div style="font-size:11.5px;line-height:1.5;margin-top:8px;color:${labelColor};opacity:0.85;">${wrap(summary)}\u2026</div>` : '')
2076
+ );
2077
+ }
2078
+ if (params.dataType === 'edge') {
2079
+ return `<div style="font-size:12px;">${wrap(params.data.edgeLabel || params.data.edgeType)}</div>`;
2080
+ }
2081
+ return '';
2082
+ },
2083
+ },
2084
+ legend: [{
2085
+ data: data.categories.map(c => c.name),
2086
+ textStyle: { color: labelColor, fontSize: 11 },
2087
+ top: 8,
2088
+ right: 12,
2089
+ itemWidth: 10,
2090
+ itemHeight: 10,
2091
+ itemGap: 12,
2092
+ icon: 'circle',
2093
+ selectedMode: false,
2094
+ }],
2095
+ animationDuration: 500,
2096
+ animationEasingUpdate: 'quinticInOut',
2097
+ series: [{
2098
+ type: 'graph',
2099
+ layout: 'none',
2100
+ roam: true,
2101
+ draggable: true,
2102
+ zoom: 1,
2103
+ scaleLimit: { min: 0.2, max: 4 },
2104
+ edgeSymbol: ['none', 'arrow'],
2105
+ edgeSymbolSize: [0, 8],
2106
+ categories: data.categories,
2107
+ data: data.nodes,
2108
+ edges: data.links,
2109
+ label: {
2110
+ show: true,
2111
+ position: 'right',
2112
+ formatter: '{b}',
2113
+ color: labelColor,
2114
+ fontSize: 11,
2115
+ fontFamily: 'Inter, system-ui, sans-serif',
2116
+ backgroundColor: light ? 'rgba(255,255,255,0.82)' : 'rgba(15,15,23,0.72)',
2117
+ padding: [2, 5],
2118
+ borderRadius: 3,
2119
+ },
2120
+ labelLayout: { hideOverlap: true, moveOverlap: 'shiftY' },
2121
+ itemStyle: {
2122
+ opacity: 0.92,
2123
+ borderColor: light ? 'rgba(0,0,0,0.22)' : 'rgba(255,255,255,0.2)',
2124
+ borderWidth: 1,
2125
+ shadowColor: light ? 'rgba(0,0,0,0.18)' : 'rgba(0,0,0,0.45)',
2126
+ shadowBlur: 8,
2127
+ },
2128
+ emphasis: {
2129
+ focus: 'adjacency',
2130
+ label: { show: true, fontWeight: 700, fontSize: 12 },
2131
+ itemStyle: { borderColor: light ? '#6750A4' : '#D0BCFF', borderWidth: 2 },
2132
+ lineStyle: { width: 2.5, opacity: 1 },
2133
+ },
2134
+ blur: {
2135
+ itemStyle: { opacity: 0.18 },
2136
+ label: { opacity: 0.25 },
2137
+ lineStyle: { opacity: 0.06 },
2138
+ },
2139
+ lineStyle: { opacity: 0.55, curveness: 0.15, width: 1.2 },
2140
+ }],
2141
+ };
2142
+
2143
+ echartsInstance.setOption(option);
2144
+
2145
+ // Click handlers — use ECharts event API (auto-cleaned on dispose) instead of zrender
2146
+ echartsInstance.on('click', (params) => {
2147
+ if (params.dataType === 'node') {
2148
+ const id = params.data?.id;
2149
+ if (id) {
2150
+ selectedNodeId = id;
2151
+ showKGInspector(id);
2152
+ }
2153
+ }
2154
+ });
2155
+ // Click on blank canvas deselects (ECharts emits click with empty target at chart level).
2156
+ // Use `click` event at series level only; do NOT intercept zrender events so roam/pan works.
2157
+ // User reported pan-on-empty-canvas not working when zr click was intercepted.
2158
+
2159
+ // Resize observer
2160
+ if (echartsResizeObserver) {
2161
+ try { echartsResizeObserver.disconnect(); } catch (_) { /* noop */ }
2162
+ }
2163
+ if (typeof ResizeObserver !== 'undefined') {
2164
+ echartsResizeObserver = new ResizeObserver(() => {
2165
+ if (echartsInstance) echartsInstance.resize();
2166
+ });
2167
+ echartsResizeObserver.observe(mountEl);
2168
+ }
2169
+
2170
+ window._kgEChart = echartsInstance;
2171
+ window._kgShowInspector = showKGInspector;
2172
+
2173
+ updateStatusBar();
2174
+ bindZoomControls();
2175
+ }
2176
+
2177
+ function bindZoomControls() {
2178
+ const outBtn = document.getElementById('gz-out');
2179
+ const fitBtn = document.getElementById('gz-fit');
2180
+ const inBtn = document.getElementById('gz-in');
2181
+ const setZoom = (factor) => {
2182
+ if (!echartsInstance) return;
2183
+ const opt = echartsInstance.getOption();
2184
+ const cur = (opt.series && opt.series[0] && opt.series[0].zoom) || 1;
2185
+ echartsInstance.setOption({ series: [{ zoom: Math.min(4, Math.max(0.2, cur * factor)) }] });
2186
+ };
2187
+ if (outBtn) outBtn.onclick = () => setZoom(0.75);
2188
+ if (fitBtn) fitBtn.onclick = () => {
2189
+ if (echartsInstance) echartsInstance.setOption({ series: [{ zoom: 1, center: null }] });
2190
+ };
2191
+ if (inBtn) inBtn.onclick = () => setZoom(1.35);
2192
+ }
2193
+
2194
+ function updateStatusBar() {
2195
+ const gsNodes = document.getElementById('gs-nodes');
2196
+ const gsEdges = document.getElementById('gs-edges');
2197
+ const gsClusters = document.getElementById('gs-clusters');
2198
+ const shownNodes = focusedMode ? Math.min(kg.nodes.length, FOCUSED_TOP_N) : kg.nodes.length;
2199
+ const shownLabel = focusedMode && kg.nodes.length > FOCUSED_TOP_N ? `${shownNodes}/${kg.nodes.length}` : `${kg.nodes.length}`;
2200
+ if (gsNodes) gsNodes.textContent = `${shownLabel} ${t('kgNodes')}`;
2201
+ const edgeLabel = lastRenderedCounts.edges < kg.edges.length
2202
+ ? `${lastRenderedCounts.edges}/${kg.edges.length}`
2203
+ : `${kg.edges.length}`;
2204
+ if (gsEdges) gsEdges.textContent = `${edgeLabel} ${t('kgEdges')}`;
2205
+ if (gsClusters) gsClusters.textContent = `${(kg.clusters || []).length} ${t('kgClusters')}`;
2206
+ }
2207
+
2208
+ // Inspector
2209
+ function showKGInspector(nodeId) {
2210
+ const inspector = document.getElementById('graph-inspector');
2211
+ if (!inspector) return;
2212
+ if (!nodeId) {
2213
+ inspector.innerHTML = '<div class="gi-empty"><div class="gi-empty-icon">\u2B21</div>' + t('graphSelectNode') + '</div>';
2214
+ return;
2215
+ }
2216
+ const node = kg.nodes.find(n => n.id === nodeId);
2217
+ if (!node) {
2218
+ inspector.innerHTML = '<div class="gi-empty"><div class="gi-empty-icon">\u2B21</div>' + t('graphSelectNode') + '</div>';
2219
+ return;
2220
+ }
2221
+ const color = getSectionColor(node.sectionId);
2222
+ const relatedEdges = kg.edges.filter(e => e.source === nodeId || e.target === nodeId);
2223
+
2224
+ const refsHtml = (node.refs || []).length > 0
2225
+ ? node.refs.map(r => `<span class="knowledge-ref-chip" data-kind="${escapeHtml(r.kind)}">${escapeHtml(r.id)}</span>`).join('')
2226
+ : '<span style="font-size:12px;color:var(--text-muted);">—</span>';
2227
+
2228
+ const edgeHtml = relatedEdges.length > 0
2229
+ ? relatedEdges.map(e => {
2230
+ const dir = e.source === nodeId;
2231
+ const other = dir ? e.target : e.source;
2232
+ const otherNode = kg.nodes.find(n => n.id === other);
2233
+ const style = edgeStyleMap[e.edgeType] || {};
2234
+ return `<div class="gi-rel-item">
2235
+ <span class="gi-rel-arrow">${dir ? '\u2192' : '\u2190'}</span>
2236
+ <span class="gi-rel-type" style="color:${style.color || 'var(--text-muted)'}">${escapeHtml(style.label || e.edgeType)}</span>
2237
+ <span class="gi-rel-target" data-kg-nav="${escapeHtml(other)}">${escapeHtml(otherNode?.label || other)}</span>
2238
+ </div>`;
2239
+ }).join('')
2240
+ : '<div style="font-size:12px;color:var(--text-muted);font-style:italic;">' + t('kgInspectorNoEdges') + '</div>';
2241
+
2242
+ inspector.innerHTML = `
2243
+ <div class="gi-header">
2244
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:4px;">
2245
+ <span style="width:10px;height:10px;border-radius:50%;background:${color};flex-shrink:0;"></span>
2246
+ <div class="gi-name">${escapeHtml(node.label)}</div>
2247
+ </div>
2248
+ <div class="gi-type">${escapeHtml(node.nodeType)}</div>
2249
+ </div>
2250
+ <div class="gi-stats">
2251
+ <div class="gi-stat"><div class="gi-stat-value">${node.evidenceCount}</div><div class="gi-stat-label">${t('kgInspectorEvidence')}</div></div>
2252
+ <div class="gi-stat"><div class="gi-stat-value">${relatedEdges.length}</div><div class="gi-stat-label">${t('kgInspectorRelatedEdges')}</div></div>
2253
+ </div>
2254
+ <div class="gi-section">
2255
+ <div class="gi-section-title">${t('kgInspectorSection')}</div>
2256
+ <div style="font-size:12px;color:${color};font-weight:500;">${escapeHtml(sectionLabel(node.sectionId))}</div>
2257
+ </div>
2258
+ ${node.entityName ? `<div class="gi-section"><div class="gi-section-title">${t('kgInspectorEntity')}</div><div style="font-size:12px;color:var(--accent-cyan);font-family:var(--font-mono);">${escapeHtml(node.entityName)}</div></div>` : ''}
2259
+ <div class="gi-section">
2260
+ <div class="gi-section-title">${t('kgInspectorSummary')}</div>
2261
+ <div style="font-size:12px;color:var(--text-secondary);line-height:1.5;">${escapeHtml(node.summary || '—')}</div>
2262
+ </div>
2263
+ <div class="gi-section">
2264
+ <div class="gi-section-title">${t('kgInspectorProvenance')}</div>
2265
+ <div class="knowledge-ref-list">${refsHtml}</div>
2266
+ </div>
2267
+ <div class="gi-section">
2268
+ <div class="gi-section-title">${t('kgInspectorRelatedEdges')} <span class="gi-section-count">${relatedEdges.length}</span></div>
2269
+ ${edgeHtml}
2270
+ </div>
2271
+ `;
2272
+
2273
+ inspector.querySelectorAll('[data-kg-nav]').forEach(el => {
2274
+ el.addEventListener('click', () => {
2275
+ const targetId = el.dataset.kgNav;
2276
+ if (echartsInstance) {
2277
+ try {
2278
+ echartsInstance.dispatchAction({ type: 'highlight', seriesIndex: 0, dataType: 'node', name: kg.nodes.find(n => n.id === targetId)?.label });
2279
+ } catch (_) { /* noop */ }
2280
+ }
2281
+ showKGInspector(targetId);
2282
+ });
2283
+ });
2284
+ }
2285
+
2286
+ // Filter panel
2287
+ function renderKGFilterPanel() {
2288
+ const panel = document.getElementById('kg-filter-panel');
2289
+ if (!panel) return;
2290
+
2291
+ const clusterEntries = (kg.clusters || []).map(c => [c.sectionId, c]);
2292
+ const clusterHtml = `
2293
+ <div class="gfp-section">
2294
+ <div class="gfp-label">${t('kgClusterFilter')}</div>
2295
+ <div class="gfp-radio-group">
2296
+ ${clusterEntries.map(([id, cluster]) => `
2297
+ <button class="gfp-check${activeSections.has(id) ? ' active' : ''}" data-section-filter="${escapeHtml(id)}">
2298
+ <span class="gfp-check-box">\u2713</span>
2299
+ <span class="gfp-type-dot" style="background:${getSectionColor(id)}"></span>
2300
+ ${escapeHtml(sectionLabel(cluster.sectionId))}
2301
+ <span class="gfp-check-count">${cluster.nodeCount}</span>
2302
+ </button>
2303
+ `).join('')}
2304
+ </div>
2305
+ </div>
2306
+ `;
2307
+
2308
+ const edgeTypeEntries = Object.entries(edgeStyleMap);
2309
+ const edgeTypeHtml = `
2310
+ <div class="gfp-section">
2311
+ <div class="gfp-label">${t('kgEdgeTypeFilter')}</div>
2312
+ <div class="gfp-radio-group">
2313
+ ${edgeTypeEntries.map(([type, style]) => `
2314
+ <button class="gfp-check${activeEdgeTypes.has(type) ? ' active' : ''}" data-edge-type-filter="${escapeHtml(type)}">
2315
+ <span class="gfp-check-box">\u2713</span>
2316
+ <span style="color:${style.color};font-size:11px;">\u2192</span>
2317
+ ${escapeHtml(style.label)}
2318
+ <span class="gfp-check-count">${kg.edges.filter(e => e.edgeType === type).length}</span>
2319
+ </button>
2320
+ `).join('')}
2321
+ </div>
2322
+ </div>
2323
+ `;
2324
+
2325
+ const searchHtml = `
2326
+ <div class="gfp-section">
2327
+ <div class="gfp-label">${t('graphSearch')}</div>
2328
+ <input type="text" class="gfp-search" id="kg-search" placeholder="${t('graphFindEntity')}" autocomplete="off" />
2329
+ </div>
2330
+ `;
2331
+
2332
+ const viewModeHtml = `
2333
+ <div class="gfp-section">
2334
+ <div class="gfp-label">${t('kgViewMode')}</div>
2335
+ <div class="gfp-depth-row">
2336
+ <button class="gfp-depth-btn${focusedMode ? ' active' : ''}" data-view-mode="focused">${t('kgFocused')}</button>
2337
+ <button class="gfp-depth-btn${!focusedMode ? ' active' : ''}" data-view-mode="full">${t('kgFullGraph')}</button>
2338
+ </div>
2339
+ </div>
2340
+ `;
2341
+
2342
+ panel.innerHTML = searchHtml + viewModeHtml + clusterHtml + edgeTypeHtml;
2343
+
2344
+ // Bind section filters
2345
+ panel.querySelectorAll('[data-section-filter]').forEach(btn => {
2346
+ btn.addEventListener('click', () => {
2347
+ const id = btn.dataset.sectionFilter;
2348
+ if (activeSections.has(id)) activeSections.delete(id);
2349
+ else activeSections.add(id);
2350
+ initEChartsGraph();
2351
+ renderKGFilterPanel();
2352
+ });
2353
+ });
2354
+
2355
+ // Bind edge type filters
2356
+ panel.querySelectorAll('[data-edge-type-filter]').forEach(btn => {
2357
+ btn.addEventListener('click', () => {
2358
+ const type = btn.dataset.edgeTypeFilter;
2359
+ if (activeEdgeTypes.has(type)) activeEdgeTypes.delete(type);
2360
+ else activeEdgeTypes.add(type);
2361
+ initEChartsGraph();
2362
+ renderKGFilterPanel();
2363
+ });
2364
+ });
2365
+
2366
+ // Bind view mode toggle
2367
+ panel.querySelectorAll('[data-view-mode]').forEach(btn => {
2368
+ btn.addEventListener('click', () => {
2369
+ const mode = btn.dataset.viewMode;
2370
+ focusedMode = mode === 'focused';
2371
+ initEChartsGraph();
2372
+ renderKGFilterPanel();
2373
+ });
2374
+ });
2375
+
2376
+ // Bind search
2377
+ const searchInput = document.getElementById('kg-search');
2378
+ if (searchInput) {
2379
+ searchInput.addEventListener('input', () => {
2380
+ const q = searchInput.value.toLowerCase();
2381
+ if (!echartsInstance) return;
2382
+ try {
2383
+ echartsInstance.dispatchAction({ type: 'downplay', seriesIndex: 0 });
2384
+ } catch (_) { /* noop */ }
2385
+ if (!q) return;
2386
+ // Highlight matched nodes (which auto-blurs others via emphasis.focus 'adjacency' isn't ideal here;
2387
+ // instead, use 'highlight' on matching dataIndex array for visual emphasis)
2388
+ const data = buildEChartsData();
2389
+ const matchIndexes = [];
2390
+ data.nodes.forEach((n, idx) => {
2391
+ const match = (n.fullLabel || '').toLowerCase().includes(q) ||
2392
+ (n.nodeType || '').toLowerCase().includes(q) ||
2393
+ (n.entityName || '').toLowerCase().includes(q);
2394
+ if (match) matchIndexes.push(idx);
2395
+ });
2396
+ if (matchIndexes.length > 0) {
2397
+ try {
2398
+ echartsInstance.dispatchAction({ type: 'highlight', seriesIndex: 0, dataIndex: matchIndexes });
2399
+ } catch (_) { /* noop */ }
2400
+ }
2401
+ });
2402
+ }
2403
+ }
2404
+
2405
+ initEChartsGraph();
2406
+ renderKGFilterPanel();
2407
+ }
2408
+
1485
2409
  // ============================================================
1486
2410
  // Cytoscape.js + Dagre — Focused Topology Renderer
1487
2411
  // Default: 1-hop neighborhood of top entity, dagre LR layout
@@ -2340,7 +3264,7 @@ async function loadObservations() {
2340
3264
  </div>
2341
3265
  <div style="display:flex;gap:8px;">
2342
3266
  <button class="export-btn" id="btn-batch-cleanup" title="${t('batchCleanup')}">
2343
- 🧹 ${t('batchCleanup')}
3267
+ [CLEANUP] ${t('batchCleanup')}
2344
3268
  </button>
2345
3269
  <button class="export-btn" id="btn-export" title="${t('exportData')}">
2346
3270
  <svg width="16" height="16" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"><path d="M8 2v8M4 7l4 4 4-4M2 12v2h12v-2"/></svg>
@@ -2439,16 +3363,16 @@ function renderObsList() {
2439
3363
  ${batchMode ? `<input type="checkbox" class="obs-checkbox" ${isSelected ? 'checked' : ''} onclick="event.stopPropagation(); toggleObsSelect(${obs.id});" />` : ''}
2440
3364
  <span class="obs-card-id">#${obs.id}</span>
2441
3365
  <span class="type-badge" data-type="${obs.type || 'unknown'}">
2442
- ${typeIcons[obs.type] || ''} ${obs.type || t('unknown')}
3366
+ ${typeIcons[obs.type] || '[UNKNOWN]'} ${obs.type || t('unknown')}
2443
3367
  </span>
2444
3368
  ${isLow ? '<span class="low-quality-badge">' + t('lowQuality') + '</span>' : ''}
2445
3369
  <span class="obs-card-title">${hl(obs.title || t('untitled'))}</span>
2446
3370
  <span class="obs-expand-icon">▼</span>
2447
3371
  </div>
2448
3372
  <div class="obs-card-meta">
2449
- <span>📁 ${hl(obs.entityName || t('unknown'))}</span>
2450
- ${obs.createdAt ? `<span>🕐 ${formatTime(obs.createdAt)}</span>` : ''}
2451
- ${obs.accessCount ? `<span>👁 ${obs.accessCount}</span>` : ''}
3373
+ <span>[FILES] ${hl(obs.entityName || t('unknown'))}</span>
3374
+ ${obs.createdAt ? `<span>[TIME] ${formatTime(obs.createdAt)}</span>` : ''}
3375
+ ${obs.accessCount ? `<span>[VIEW] ${obs.accessCount}</span>` : ''}
2452
3376
  </div>
2453
3377
  <div class="obs-detail" id="obs-detail-${obs.id}" style="display:none;">
2454
3378
  <div class="obs-detail-inner">
@@ -2479,7 +3403,7 @@ async function loadRetention() {
2479
3403
 
2480
3404
  const data = await api('retention');
2481
3405
  if (!data || data.items.length === 0) {
2482
- container.innerHTML = emptyState('📉', t('noRetentionData'), t('noRetentionDesc'));
3406
+ container.innerHTML = emptyState('[RETENTION]', t('noRetentionData'), t('noRetentionDesc'));
2483
3407
  return;
2484
3408
  }
2485
3409
 
@@ -2647,7 +3571,7 @@ async function loadGitMemory() {
2647
3571
 
2648
3572
  const [stats, allObs] = await Promise.all([api('stats'), api('observations')]);
2649
3573
  if (!stats || !allObs) {
2650
- container.innerHTML = emptyState('🔀', t('noGitMemory'), t('noGitMemoryDesc'));
3574
+ container.innerHTML = emptyState('[MERGE]', t('noGitMemory'), t('noGitMemoryDesc'));
2651
3575
  return;
2652
3576
  }
2653
3577
 
@@ -2689,7 +3613,7 @@ async function loadGitMemory() {
2689
3613
  ${gitObs.length === 0 ? `
2690
3614
  <div class="panel">
2691
3615
  <div class="panel-body" style="text-align:center;padding:48px;">
2692
- <div style="font-size:36px;margin-bottom:12px;">🔀</div>
3616
+ <div style="font-size:36px;margin-bottom:12px;">[MERGE]</div>
2693
3617
  <div style="font-size:16px;font-weight:600;color:var(--text-primary);margin-bottom:8px;">${t('noGitMemoriesYet')}</div>
2694
3618
  <div style="font-size:13px;color:var(--text-muted);max-width:400px;margin:0 auto;">
2695
3619
  ${t('noGitMemoriesHint')}<br>
@@ -3184,7 +4108,7 @@ async function loadTeam() {
3184
4108
  <div style="font-size:16px;font-weight:600;color:var(--text-primary);margin-bottom:8px;">${t('teamNoData')}</div>
3185
4109
  <div style="font-size:13px;color:var(--text-muted);max-width:480px;margin:0 auto;line-height:1.6;">
3186
4110
  ${t('teamNoDataHint')}<br>
3187
- <code style="background:var(--bg-surface);padding:4px 10px;border-radius:6px;margin-top:8px;display:inline-block;font-size:12px;">memorix serve-http --port 3211</code>
4111
+ <code style="background:var(--bg-surface);padding:4px 10px;border-radius:6px;margin-top:8px;display:inline-block;font-size:12px;">memorix orchestrate · memorix team status · memorix task list</code>
3188
4112
  </div>
3189
4113
  </div>
3190
4114
  </div>