@archznn/crewloop-skills 0.6.0 → 0.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.
Files changed (245) hide show
  1. package/README.md +4 -16
  2. package/package.json +1 -2
  3. package/packages/cli/dist/agents.js +1 -1
  4. package/packages/cli/dist/agents.js.map +1 -1
  5. package/packages/cli/dist/cli.d.ts.map +1 -1
  6. package/packages/cli/dist/cli.js +2 -30
  7. package/packages/cli/dist/cli.js.map +1 -1
  8. package/packages/cli/dist/hooks.d.ts +6 -4
  9. package/packages/cli/dist/hooks.d.ts.map +1 -1
  10. package/packages/cli/dist/hooks.js +250 -98
  11. package/packages/cli/dist/hooks.js.map +1 -1
  12. package/packages/cli/dist/tests/hooks.test.js +245 -33
  13. package/packages/cli/dist/tests/hooks.test.js.map +1 -1
  14. package/references/conventions.md +1 -10
  15. package/references/workflow.md +1 -1
  16. package/servers/dashboard/README.md +55 -1
  17. package/servers/dashboard/dist/adapters/agy.d.ts +19 -0
  18. package/servers/dashboard/dist/adapters/agy.d.ts.map +1 -0
  19. package/servers/dashboard/dist/adapters/agy.js +108 -0
  20. package/servers/dashboard/dist/adapters/agy.js.map +1 -0
  21. package/servers/dashboard/dist/adapters/codex.d.ts.map +1 -1
  22. package/servers/dashboard/dist/adapters/codex.js +2 -0
  23. package/servers/dashboard/dist/adapters/codex.js.map +1 -1
  24. package/servers/dashboard/dist/adapters/kimi.d.ts +1 -1
  25. package/servers/dashboard/dist/adapters/kimi.d.ts.map +1 -1
  26. package/servers/dashboard/dist/adapters/kimi.js +9 -0
  27. package/servers/dashboard/dist/adapters/kimi.js.map +1 -1
  28. package/servers/dashboard/dist/adapters/shim.d.ts +1 -1
  29. package/servers/dashboard/dist/adapters/shim.d.ts.map +1 -1
  30. package/servers/dashboard/dist/adapters/shim.js +32 -11
  31. package/servers/dashboard/dist/adapters/shim.js.map +1 -1
  32. package/servers/dashboard/dist/adapters/shim.test.js +46 -4
  33. package/servers/dashboard/dist/adapters/shim.test.js.map +1 -1
  34. package/servers/dashboard/dist/lib/constants.d.ts +5 -0
  35. package/servers/dashboard/dist/lib/constants.d.ts.map +1 -0
  36. package/servers/dashboard/dist/lib/constants.js +46 -0
  37. package/servers/dashboard/dist/lib/constants.js.map +1 -0
  38. package/servers/dashboard/dist/lib/format.d.ts +6 -0
  39. package/servers/dashboard/dist/lib/format.d.ts.map +1 -0
  40. package/servers/dashboard/dist/lib/format.js +52 -0
  41. package/servers/dashboard/dist/lib/format.js.map +1 -0
  42. package/servers/dashboard/dist/lib/graph.d.ts +22 -0
  43. package/servers/dashboard/dist/lib/graph.d.ts.map +1 -0
  44. package/servers/dashboard/dist/lib/graph.js +45 -0
  45. package/servers/dashboard/dist/lib/graph.js.map +1 -0
  46. package/servers/dashboard/dist/lib/invocations.d.ts +32 -0
  47. package/servers/dashboard/dist/lib/invocations.d.ts.map +1 -0
  48. package/servers/dashboard/dist/lib/invocations.js +135 -0
  49. package/servers/dashboard/dist/lib/invocations.js.map +1 -0
  50. package/servers/dashboard/dist/lib/invocations.test.d.ts +2 -0
  51. package/servers/dashboard/dist/lib/invocations.test.d.ts.map +1 -0
  52. package/servers/dashboard/dist/lib/invocations.test.js +68 -0
  53. package/servers/dashboard/dist/lib/invocations.test.js.map +1 -0
  54. package/servers/dashboard/dist/lib/paths.d.ts +2 -0
  55. package/servers/dashboard/dist/lib/paths.d.ts.map +1 -0
  56. package/servers/dashboard/dist/lib/paths.js +40 -0
  57. package/servers/dashboard/dist/lib/paths.js.map +1 -0
  58. package/servers/dashboard/dist/presenter.d.ts.map +1 -1
  59. package/servers/dashboard/dist/presenter.js +2 -0
  60. package/servers/dashboard/dist/presenter.js.map +1 -1
  61. package/servers/dashboard/dist/public/assets/index-DjmMKbPN.css +1 -0
  62. package/servers/dashboard/dist/public/assets/index-DzOqMleZ.js +5323 -0
  63. package/servers/dashboard/dist/public/assets/index-DzOqMleZ.js.map +1 -0
  64. package/servers/dashboard/dist/public/index.html +16 -0
  65. package/servers/dashboard/dist/server.d.ts.map +1 -1
  66. package/servers/dashboard/dist/server.js +5 -1
  67. package/servers/dashboard/dist/server.js.map +1 -1
  68. package/servers/dashboard/dist/skills/infer.d.ts.map +1 -1
  69. package/servers/dashboard/dist/skills/infer.js +0 -6
  70. package/servers/dashboard/dist/skills/infer.js.map +1 -1
  71. package/servers/dashboard/dist/skills/infer.test.js +10 -3
  72. package/servers/dashboard/dist/skills/infer.test.js.map +1 -1
  73. package/servers/dashboard/dist/skills/mapping.d.ts +0 -3
  74. package/servers/dashboard/dist/skills/mapping.d.ts.map +1 -1
  75. package/servers/dashboard/dist/skills/mapping.js +0 -18
  76. package/servers/dashboard/dist/skills/mapping.js.map +1 -1
  77. package/servers/dashboard/dist/skills/registry.d.ts.map +1 -1
  78. package/servers/dashboard/dist/skills/registry.js +0 -1
  79. package/servers/dashboard/dist/skills/registry.js.map +1 -1
  80. package/servers/dashboard/dist/tests/adapters.test.d.ts +2 -0
  81. package/servers/dashboard/dist/tests/adapters.test.d.ts.map +1 -0
  82. package/servers/dashboard/dist/tests/adapters.test.js +180 -0
  83. package/servers/dashboard/dist/tests/adapters.test.js.map +1 -0
  84. package/servers/dashboard/dist/tests/lib-helpers.test.d.ts +2 -0
  85. package/servers/dashboard/dist/tests/lib-helpers.test.d.ts.map +1 -0
  86. package/servers/dashboard/dist/tests/lib-helpers.test.js +123 -0
  87. package/servers/dashboard/dist/tests/lib-helpers.test.js.map +1 -0
  88. package/servers/dashboard/dist/tests/shim.test.js +88 -2
  89. package/servers/dashboard/dist/tests/shim.test.js.map +1 -1
  90. package/servers/dashboard/dist/types.d.ts +5 -2
  91. package/servers/dashboard/dist/types.d.ts.map +1 -1
  92. package/servers/dashboard/package.json +22 -5
  93. package/servers/dashboard/src/adapters/agy.ts +136 -0
  94. package/servers/dashboard/src/adapters/codex.ts +2 -0
  95. package/servers/dashboard/src/adapters/kimi.ts +11 -1
  96. package/servers/dashboard/src/adapters/shim.test.ts +57 -4
  97. package/servers/dashboard/src/adapters/shim.ts +31 -11
  98. package/servers/dashboard/src/lib/constants.ts +44 -0
  99. package/servers/dashboard/src/lib/format.ts +44 -0
  100. package/servers/dashboard/src/lib/graph.ts +69 -0
  101. package/servers/dashboard/src/lib/invocations.test.ts +70 -0
  102. package/servers/dashboard/src/lib/invocations.ts +172 -0
  103. package/servers/dashboard/src/lib/paths.ts +35 -0
  104. package/servers/dashboard/src/presenter.ts +2 -0
  105. package/servers/dashboard/src/server.ts +5 -1
  106. package/servers/dashboard/src/skills/infer.test.ts +11 -3
  107. package/servers/dashboard/src/skills/infer.ts +1 -8
  108. package/servers/dashboard/src/skills/mapping.ts +0 -20
  109. package/servers/dashboard/src/skills/registry.ts +0 -1
  110. package/servers/dashboard/src/tests/adapters.test.ts +198 -0
  111. package/servers/dashboard/src/tests/lib-helpers.test.ts +133 -0
  112. package/servers/dashboard/src/tests/shim.test.ts +110 -2
  113. package/servers/dashboard/src/types.ts +5 -3
  114. package/servers/dashboard/ui/index.html +15 -0
  115. package/servers/dashboard/ui/postcss.config.js +6 -0
  116. package/servers/dashboard/ui/src/App.tsx +360 -0
  117. package/servers/dashboard/ui/src/components/ActiveSkillPanel.tsx +69 -0
  118. package/servers/dashboard/ui/src/components/ActivityGraph.tsx +74 -0
  119. package/servers/dashboard/ui/src/components/CommandPalette.tsx +200 -0
  120. package/servers/dashboard/ui/src/components/FileActivity.tsx +20 -0
  121. package/servers/dashboard/ui/src/components/FileDiff.tsx +68 -0
  122. package/servers/dashboard/ui/src/components/FileList.tsx +64 -0
  123. package/servers/dashboard/ui/src/components/FilterBar.tsx +208 -0
  124. package/servers/dashboard/ui/src/components/Network3D.tsx +178 -0
  125. package/servers/dashboard/ui/src/components/SessionSelector.tsx +95 -0
  126. package/servers/dashboard/ui/src/components/Sidebar.tsx +110 -0
  127. package/servers/dashboard/ui/src/components/TelemetryPanel.tsx +57 -0
  128. package/servers/dashboard/ui/src/components/Timeline.tsx +57 -0
  129. package/servers/dashboard/ui/src/components/TimelineRow.tsx +112 -0
  130. package/servers/dashboard/ui/src/components/TopBar.tsx +116 -0
  131. package/servers/dashboard/ui/src/components/ViewHeader.tsx +19 -0
  132. package/servers/dashboard/ui/src/components/ui/Icon.tsx +105 -0
  133. package/servers/dashboard/ui/src/components/ui/StatusBadge.tsx +19 -0
  134. package/servers/dashboard/ui/src/components/views/FilesView.tsx +23 -0
  135. package/servers/dashboard/ui/src/components/views/NetworkView.tsx +20 -0
  136. package/servers/dashboard/ui/src/components/views/Overview.tsx +135 -0
  137. package/servers/dashboard/ui/src/components/views/SessionsView.tsx +84 -0
  138. package/servers/dashboard/ui/src/components/views/SettingsView.tsx +138 -0
  139. package/servers/dashboard/ui/src/components/views/SkillsView.tsx +92 -0
  140. package/servers/dashboard/ui/src/components/views/TimelineView.tsx +46 -0
  141. package/servers/dashboard/ui/src/contexts/FilterContext.tsx +41 -0
  142. package/servers/dashboard/ui/src/contexts/PinnedSessionsContext.tsx +80 -0
  143. package/servers/dashboard/ui/src/contexts/SettingsContext.tsx +60 -0
  144. package/servers/dashboard/ui/src/hooks/useCommandPalette.ts +36 -0
  145. package/servers/dashboard/ui/src/hooks/useKeyboardShortcut.ts +38 -0
  146. package/servers/dashboard/ui/src/hooks/useNow.ts +12 -0
  147. package/servers/dashboard/ui/src/hooks/useReducedMotion.ts +15 -0
  148. package/servers/dashboard/ui/src/hooks/useSessions.ts +64 -0
  149. package/servers/dashboard/ui/src/hooks/useTheme.ts +30 -0
  150. package/servers/dashboard/ui/src/hooks/useViewport.ts +19 -0
  151. package/servers/dashboard/ui/src/hooks/useWebSocket.ts +118 -0
  152. package/servers/dashboard/ui/src/lib/export.test.ts +33 -0
  153. package/servers/dashboard/ui/src/lib/export.ts +39 -0
  154. package/servers/dashboard/ui/src/lib/filter.test.ts +95 -0
  155. package/servers/dashboard/ui/src/lib/filter.ts +178 -0
  156. package/servers/dashboard/ui/src/lib/format.test.ts +25 -0
  157. package/servers/dashboard/ui/src/lib/search.test.ts +52 -0
  158. package/servers/dashboard/ui/src/lib/search.ts +60 -0
  159. package/servers/dashboard/ui/src/lib/settings.test.ts +50 -0
  160. package/servers/dashboard/ui/src/lib/settings.ts +56 -0
  161. package/servers/dashboard/ui/src/lib/types.ts +124 -0
  162. package/servers/dashboard/ui/src/main.tsx +19 -0
  163. package/servers/dashboard/ui/src/styles/index.css +155 -0
  164. package/servers/dashboard/ui/tailwind.config.js +45 -0
  165. package/servers/dashboard/ui/tsconfig.json +33 -0
  166. package/servers/dashboard/ui/tsconfig.node.json +10 -0
  167. package/servers/dashboard/ui/vite.config.ts +37 -0
  168. package/servers/dashboard/ui/vitest.config.ts +8 -0
  169. package/skills/accessibility-auditor/SKILL.md +0 -20
  170. package/skills/architect/SKILL.md +0 -45
  171. package/skills/designer/SKILL.md +0 -30
  172. package/skills/docs-writer/SKILL.md +0 -13
  173. package/skills/engineer/SKILL.md +0 -30
  174. package/skills/maintainer/SKILL.md +0 -20
  175. package/skills/orchestrator/SKILL.md +0 -13
  176. package/skills/product-manager/SKILL.md +0 -20
  177. package/skills/researcher/SKILL.md +0 -20
  178. package/skills/reviewer/SKILL.md +0 -30
  179. package/skills/security-guard/SKILL.md +0 -20
  180. package/skills/shipper/SKILL.md +0 -33
  181. package/skills/tester/SKILL.md +0 -20
  182. package/packages/cli/dist/mcp.d.ts +0 -28
  183. package/packages/cli/dist/mcp.d.ts.map +0 -1
  184. package/packages/cli/dist/mcp.js +0 -148
  185. package/packages/cli/dist/mcp.js.map +0 -1
  186. package/packages/cli/dist/tests/mcp.test.d.ts +0 -2
  187. package/packages/cli/dist/tests/mcp.test.d.ts.map +0 -1
  188. package/packages/cli/dist/tests/mcp.test.js +0 -232
  189. package/packages/cli/dist/tests/mcp.test.js.map +0 -1
  190. package/references/obsidian-mcp-usage.md +0 -190
  191. package/servers/dashboard/public/app.js +0 -516
  192. package/servers/dashboard/public/index.html +0 -96
  193. package/servers/dashboard/public/styles.css +0 -819
  194. package/servers/obsidian-mcp/README.md +0 -82
  195. package/servers/obsidian-mcp/pyproject.toml +0 -32
  196. package/servers/obsidian-mcp/src/obsidian_mcp/__init__.py +0 -0
  197. package/servers/obsidian-mcp/src/obsidian_mcp/config.py +0 -47
  198. package/servers/obsidian-mcp/src/obsidian_mcp/indexer/__init__.py +0 -0
  199. package/servers/obsidian-mcp/src/obsidian_mcp/indexer/embeddings.py +0 -105
  200. package/servers/obsidian-mcp/src/obsidian_mcp/indexer/indexer.py +0 -79
  201. package/servers/obsidian-mcp/src/obsidian_mcp/indexer/store.py +0 -141
  202. package/servers/obsidian-mcp/src/obsidian_mcp/indexer/sync.py +0 -37
  203. package/servers/obsidian-mcp/src/obsidian_mcp/learning/__init__.py +0 -0
  204. package/servers/obsidian-mcp/src/obsidian_mcp/learning/detector.py +0 -66
  205. package/servers/obsidian-mcp/src/obsidian_mcp/learning/note_generator.py +0 -40
  206. package/servers/obsidian-mcp/src/obsidian_mcp/main.py +0 -4
  207. package/servers/obsidian-mcp/src/obsidian_mcp/models.py +0 -42
  208. package/servers/obsidian-mcp/src/obsidian_mcp/privacy/__init__.py +0 -0
  209. package/servers/obsidian-mcp/src/obsidian_mcp/privacy/filter.py +0 -68
  210. package/servers/obsidian-mcp/src/obsidian_mcp/rag/__init__.py +0 -0
  211. package/servers/obsidian-mcp/src/obsidian_mcp/rag/engine.py +0 -50
  212. package/servers/obsidian-mcp/src/obsidian_mcp/rag/graph_search.py +0 -55
  213. package/servers/obsidian-mcp/src/obsidian_mcp/rag/text_search.py +0 -37
  214. package/servers/obsidian-mcp/src/obsidian_mcp/rag/vector_search.py +0 -118
  215. package/servers/obsidian-mcp/src/obsidian_mcp/server.py +0 -61
  216. package/servers/obsidian-mcp/src/obsidian_mcp/tools/__init__.py +0 -0
  217. package/servers/obsidian-mcp/src/obsidian_mcp/tools/create.py +0 -43
  218. package/servers/obsidian-mcp/src/obsidian_mcp/tools/delete.py +0 -16
  219. package/servers/obsidian-mcp/src/obsidian_mcp/tools/learn.py +0 -42
  220. package/servers/obsidian-mcp/src/obsidian_mcp/tools/list.py +0 -16
  221. package/servers/obsidian-mcp/src/obsidian_mcp/tools/read.py +0 -15
  222. package/servers/obsidian-mcp/src/obsidian_mcp/tools/registry.py +0 -130
  223. package/servers/obsidian-mcp/src/obsidian_mcp/tools/related.py +0 -20
  224. package/servers/obsidian-mcp/src/obsidian_mcp/tools/search.py +0 -26
  225. package/servers/obsidian-mcp/src/obsidian_mcp/tools/sync.py +0 -22
  226. package/servers/obsidian-mcp/src/obsidian_mcp/tools/update.py +0 -34
  227. package/servers/obsidian-mcp/src/obsidian_mcp/vault/__init__.py +0 -0
  228. package/servers/obsidian-mcp/src/obsidian_mcp/vault/parser.py +0 -82
  229. package/servers/obsidian-mcp/src/obsidian_mcp/vault/repository.py +0 -68
  230. package/servers/obsidian-mcp/src/obsidian_mcp/vault/writer.py +0 -61
  231. package/servers/obsidian-mcp/tests/conftest.py +0 -39
  232. package/servers/obsidian-mcp/tests/test_async_tools.py +0 -87
  233. package/servers/obsidian-mcp/tests/test_edge_cases.py +0 -59
  234. package/servers/obsidian-mcp/tests/test_indexer.py +0 -27
  235. package/servers/obsidian-mcp/tests/test_integration.py +0 -90
  236. package/servers/obsidian-mcp/tests/test_learning.py +0 -34
  237. package/servers/obsidian-mcp/tests/test_privacy.py +0 -31
  238. package/servers/obsidian-mcp/tests/test_privacy_config.py +0 -44
  239. package/servers/obsidian-mcp/tests/test_rag.py +0 -64
  240. package/servers/obsidian-mcp/tests/test_read_raw.py +0 -37
  241. package/servers/obsidian-mcp/tests/test_tfidf_fallback.py +0 -54
  242. package/servers/obsidian-mcp/tests/test_tools.py +0 -108
  243. package/servers/obsidian-mcp/tests/test_vault.py +0 -103
  244. package/servers/obsidian-mcp/tests/test_writer.py +0 -139
  245. package/skills/obsidian-second-brain/SKILL.md +0 -298
@@ -0,0 +1,360 @@
1
+ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
2
+ import type { ClientSession, ClientWebSocketMessage } from '../../src/types';
3
+ import type { View, CommandPaletteItem, FilterOptions, FilterState } from './lib/types';
4
+ import { useSessions } from './hooks/useSessions';
5
+ import { useWebSocket } from './hooks/useWebSocket';
6
+ import { useSettings } from './contexts/SettingsContext';
7
+ import { usePinnedSessions } from './contexts/PinnedSessionsContext';
8
+ import { useFilters } from './contexts/FilterContext';
9
+ import { useKeyboardShortcut } from './hooks/useKeyboardShortcut';
10
+ import { useNow } from './hooks/useNow';
11
+ import { TopBar } from './components/TopBar';
12
+ import { Sidebar } from './components/Sidebar';
13
+ import { CommandPalette } from './components/CommandPalette';
14
+ import { Overview } from './components/views/Overview';
15
+ import { SessionsView } from './components/views/SessionsView';
16
+ import { TimelineView } from './components/views/TimelineView';
17
+ import { NetworkView } from './components/views/NetworkView';
18
+ import { FilesView } from './components/views/FilesView';
19
+ import { SkillsView } from './components/views/SkillsView';
20
+ import { SettingsView } from './components/views/SettingsView';
21
+ import { projectInvocations, buildFileActivity } from '../../src/lib/invocations';
22
+ import { buildGraph3D } from '../../src/lib/graph';
23
+ import { resolvePath } from '../../src/lib/paths';
24
+ import { buildOptions, filterInvocations, filterSessions, filterGraph } from './lib/filter';
25
+ import { toExportableEvent, toJson, download, filename } from './lib/export';
26
+ import { sourceIcon } from '../../src/lib/constants';
27
+ import { formatTime } from '../../src/lib/format';
28
+
29
+ const VIEWS: { key: View; label: string; icon: string }[] = [
30
+ { key: 'overview', label: 'Overview', icon: 'House' },
31
+ { key: 'sessions', label: 'Sessions', icon: 'Rows' },
32
+ { key: 'timeline', label: 'Timeline', icon: 'Clock' },
33
+ { key: 'network', label: 'Network', icon: 'Graph' },
34
+ { key: 'files', label: 'Files', icon: 'Files' },
35
+ { key: 'skills', label: 'Skills', icon: 'ChartPie' },
36
+ { key: 'settings', label: 'Settings', icon: 'Gear' },
37
+ ];
38
+
39
+ function buildPaletteItems(
40
+ currentView: View,
41
+ setView: (v: View) => void,
42
+ sessions: ClientSession[],
43
+ selectSession: (id: string) => void,
44
+ setFilters: (u: Partial<FilterState>) => void,
45
+ exportJson: () => void,
46
+ toggleDensity: () => void
47
+ ): CommandPaletteItem[] {
48
+ const items: CommandPaletteItem[] = [];
49
+
50
+ for (const v of VIEWS) {
51
+ items.push({
52
+ id: `view:${v.key}`,
53
+ type: 'view',
54
+ title: v.label,
55
+ icon: v.icon,
56
+ keywords: [v.label.toLowerCase()],
57
+ action: () => setView(v.key),
58
+ });
59
+ }
60
+
61
+ for (const s of sessions) {
62
+ items.push({
63
+ id: `session:${s.id}`,
64
+ type: 'session',
65
+ title: s.activeSkill?.name || s.id,
66
+ subtitle: `${s.source} · ${formatTime(s.startTime)}`,
67
+ icon: sourceIcon(s.source),
68
+ keywords: [s.id, s.source, s.activeSkill?.name || ''],
69
+ action: () => selectSession(s.id),
70
+ });
71
+ }
72
+
73
+ const skills = new Set<string>();
74
+ const tools = new Set<string>();
75
+ const files = new Set<string>();
76
+ const recentEvents: { id: string; title: string; tool?: string; time: number }[] = [];
77
+
78
+ for (const s of sessions) {
79
+ if (s.activeSkill?.name) skills.add(s.activeSkill.name);
80
+ if (s.skill) skills.add(s.skill);
81
+ for (const e of s.events.slice(-10)) {
82
+ if (e.tool) tools.add(e.tool);
83
+ const path = resolvePath(e.input, e.output);
84
+ if (path) files.add(path);
85
+ recentEvents.push({
86
+ id: e.id,
87
+ title: e.tool || e.event_type,
88
+ tool: e.tool,
89
+ time: e.timestamp,
90
+ });
91
+ }
92
+ }
93
+
94
+ for (const skill of skills) {
95
+ items.push({
96
+ id: `skill:${skill}`,
97
+ type: 'skill',
98
+ title: skill,
99
+ icon: 'Target',
100
+ action: () => setFilters({ skills: [skill] }),
101
+ });
102
+ }
103
+
104
+ for (const tool of tools) {
105
+ items.push({
106
+ id: `tool:${tool}`,
107
+ type: 'tool',
108
+ title: tool,
109
+ icon: 'Wrench',
110
+ action: () => setFilters({ tools: [tool] }),
111
+ });
112
+ }
113
+
114
+ for (const file of files) {
115
+ items.push({
116
+ id: `file:${file}`,
117
+ type: 'file',
118
+ title: file,
119
+ icon: 'FileText',
120
+ action: () => {
121
+ setView('files');
122
+ setFilters({ query: file });
123
+ },
124
+ });
125
+ }
126
+
127
+ recentEvents
128
+ .sort((a, b) => b.time - a.time)
129
+ .slice(0, 20)
130
+ .forEach((ev) => {
131
+ items.push({
132
+ id: `event:${ev.id}`,
133
+ type: 'event',
134
+ title: ev.title,
135
+ subtitle: formatTime(ev.time),
136
+ icon: 'Clock',
137
+ action: () => {
138
+ if (currentView !== 'timeline') setView('timeline');
139
+ setFilters({ query: ev.tool || ev.title });
140
+ },
141
+ });
142
+ });
143
+
144
+ items.push(
145
+ {
146
+ id: 'action:export',
147
+ type: 'action',
148
+ title: 'Export timeline JSON',
149
+ icon: 'DownloadSimple',
150
+ action: exportJson,
151
+ },
152
+ {
153
+ id: 'action:density',
154
+ type: 'action',
155
+ title: 'Toggle density',
156
+ icon: 'ArrowsInLineVertical',
157
+ action: toggleDensity,
158
+ },
159
+ {
160
+ id: 'action:settings',
161
+ type: 'action',
162
+ title: 'Open settings',
163
+ icon: 'Gear',
164
+ action: () => setView('settings'),
165
+ }
166
+ );
167
+
168
+ return items;
169
+ }
170
+
171
+ export default function App() {
172
+ const { settings, setSettings } = useSettings();
173
+ const { pins } = usePinnedSessions();
174
+ const { filters, setFilters, resetFilters } = useFilters();
175
+ const { sessions, selectedSessionId, selectSession, handleMessage, sortedSessions } = useSessions();
176
+ const [activeView, setActiveView] = useState<View>('overview');
177
+ const [cmdOpen, setCmdOpen] = useState(false);
178
+ const [sidebarOpen, setSidebarOpen] = useState(false);
179
+ const [timelinePaused, setTimelinePaused] = useState(false);
180
+ const [pendingUpdates, setPendingUpdates] = useState<ClientWebSocketMessage[]>([]);
181
+ const now = useNow();
182
+ const pausedRef = useRef(timelinePaused);
183
+
184
+ useEffect(() => {
185
+ pausedRef.current = timelinePaused;
186
+ }, [timelinePaused]);
187
+
188
+ const sortedWithPins = useMemo(
189
+ () => filterSessions(sortedSessions, filters, pins, now),
190
+ [sortedSessions, filters, pins, now]
191
+ );
192
+
193
+ const activeSessionId = useMemo(() => {
194
+ for (const s of sessions.values()) {
195
+ if (s.lifecycle === 'running') return s.id;
196
+ }
197
+ return sortedWithPins[0]?.id;
198
+ }, [sessions, sortedWithPins]);
199
+
200
+ const selectedSession = useMemo(
201
+ () => (selectedSessionId ? sessions.get(selectedSessionId) : undefined),
202
+ [sessions, selectedSessionId]
203
+ );
204
+
205
+ useEffect(() => {
206
+ if (settings.autoFollowActive && activeSessionId && !selectedSessionId) {
207
+ selectSession(activeSessionId);
208
+ }
209
+ }, [activeSessionId, selectedSessionId, selectSession, settings.autoFollowActive]);
210
+
211
+ const flushPending = useCallback(() => {
212
+ if (pendingUpdates.length === 0) return;
213
+ const batch = pendingUpdates;
214
+ setPendingUpdates([]);
215
+ for (const msg of batch) handleMessage(msg);
216
+ }, [pendingUpdates, handleMessage]);
217
+
218
+ useEffect(() => {
219
+ if (!timelinePaused) flushPending();
220
+ }, [timelinePaused, flushPending]);
221
+
222
+ const onMessage = useCallback(
223
+ (msg: ClientWebSocketMessage) => {
224
+ if (pausedRef.current && (msg.type === 'snapshot' || msg.type === 'update')) {
225
+ setPendingUpdates((prev) => [...prev, msg]);
226
+ return;
227
+ }
228
+ handleMessage(msg);
229
+ },
230
+ [handleMessage]
231
+ );
232
+
233
+ const { status: connection } = useWebSocket(`ws://${location.host}/ws`, onMessage);
234
+
235
+ const invocations = useMemo(
236
+ () => projectInvocations(selectedSession?.events || []),
237
+ [selectedSession]
238
+ );
239
+ const filteredInvocations = useMemo(
240
+ () => filterInvocations(invocations, selectedSession, filters, now),
241
+ [invocations, selectedSession, filters, now]
242
+ );
243
+ const filteredFiles = useMemo(
244
+ () => buildFileActivity(filteredInvocations, resolvePath),
245
+ [filteredInvocations]
246
+ );
247
+ const graph = useMemo(
248
+ () => buildGraph3D(selectedSession, filteredInvocations),
249
+ [selectedSession, filteredInvocations]
250
+ );
251
+ const filteredGraph = useMemo(
252
+ () => filterGraph(graph, filteredInvocations, filters),
253
+ [graph, filteredInvocations, filters]
254
+ );
255
+ const filterOptions = useMemo<FilterOptions>(
256
+ () => buildOptions(sessions, selectedSessionId),
257
+ [sessions, selectedSessionId]
258
+ );
259
+
260
+ function handleExport() {
261
+ const events = filteredInvocations.map(toExportableEvent);
262
+ download(toJson(events), filename('json'));
263
+ }
264
+
265
+ function toggleDensity() {
266
+ setSettings((s) => ({ ...s, density: s.density === 'compact' ? 'comfortable' : 'compact' }));
267
+ }
268
+
269
+ const paletteItems = useMemo(
270
+ () =>
271
+ buildPaletteItems(
272
+ activeView,
273
+ (v) => {
274
+ setActiveView(v);
275
+ resetFilters();
276
+ },
277
+ sortedSessions,
278
+ selectSession,
279
+ setFilters,
280
+ handleExport,
281
+ toggleDensity
282
+ ),
283
+ [activeView, sortedSessions, selectSession, setFilters, resetFilters]
284
+ );
285
+
286
+ useKeyboardShortcut('k', () => setCmdOpen(true), { meta: true });
287
+
288
+ function renderView() {
289
+ switch (activeView) {
290
+ case 'overview':
291
+ return (
292
+ <Overview
293
+ sessions={sessions}
294
+ selectedSession={selectedSession}
295
+ onSelectSession={selectSession}
296
+ />
297
+ );
298
+ case 'sessions':
299
+ return (
300
+ <SessionsView
301
+ sessions={sortedWithPins}
302
+ selectedSessionId={selectedSessionId}
303
+ filterOptions={filterOptions}
304
+ onSelectSession={selectSession}
305
+ />
306
+ );
307
+ case 'timeline':
308
+ return (
309
+ <TimelineView
310
+ invocations={filteredInvocations}
311
+ filterOptions={filterOptions}
312
+ onMouseEnter={() => setTimelinePaused(true)}
313
+ onMouseLeave={() => setTimelinePaused(false)}
314
+ />
315
+ );
316
+ case 'network':
317
+ return <NetworkView graph={filteredGraph} filterOptions={filterOptions} />;
318
+ case 'files':
319
+ return <FilesView files={filteredFiles} filterOptions={filterOptions} />;
320
+ case 'skills':
321
+ return <SkillsView invocations={filteredInvocations} filterOptions={filterOptions} />;
322
+ case 'settings':
323
+ return <SettingsView />;
324
+ default:
325
+ return null;
326
+ }
327
+ }
328
+
329
+ return (
330
+ <div className="h-screen flex flex-col bg-base text-text-primary overflow-hidden">
331
+ <TopBar
332
+ activeView={activeView}
333
+ sessions={sortedWithPins}
334
+ selectedSessionId={selectedSessionId}
335
+ activeSessionId={activeSessionId}
336
+ connection={connection}
337
+ onSelectSession={selectSession}
338
+ onOpenCommandPalette={() => setCmdOpen(true)}
339
+ onToggleSidebar={() => setSidebarOpen((v) => !v)}
340
+ />
341
+ <div className="flex-1 flex overflow-hidden">
342
+ <Sidebar
343
+ activeView={activeView}
344
+ onChange={(view) => {
345
+ setActiveView(view);
346
+ setSidebarOpen(false);
347
+ }}
348
+ mobileOpen={sidebarOpen}
349
+ onClose={() => setSidebarOpen(false)}
350
+ />
351
+ <main className="flex-1 min-w-0 overflow-hidden animate-fade-in">{renderView()}</main>
352
+ </div>
353
+ <CommandPalette
354
+ items={paletteItems}
355
+ open={cmdOpen}
356
+ onClose={() => setCmdOpen(false)}
357
+ />
358
+ </div>
359
+ );
360
+ }
@@ -0,0 +1,69 @@
1
+ import type { ClientSession } from '../../../src/types';
2
+ import { skillIcon, sourceIcon } from '../../../src/lib/constants';
3
+ import { Icon } from './ui/Icon';
4
+
5
+ interface Props {
6
+ session: ClientSession | undefined;
7
+ }
8
+
9
+ function lifecycleColor(lifecycle: string): string {
10
+ switch (lifecycle) {
11
+ case 'starting':
12
+ return 'bg-warning';
13
+ case 'running':
14
+ return 'bg-running';
15
+ case 'ended':
16
+ return 'bg-text-muted';
17
+ default:
18
+ return 'bg-text-muted';
19
+ }
20
+ }
21
+
22
+ export function ActiveSkillPanel({ session }: Props) {
23
+ if (!session) {
24
+ return (
25
+ <section className="panel p-6 flex flex-col items-center justify-center gap-3 text-center min-h-[280px]">
26
+ <div className="w-16 h-16 rounded-xl bg-elevated border border-border-default flex items-center justify-center text-text-muted">
27
+ <Icon name="MonitorPlay" className="w-8 h-8" />
28
+ </div>
29
+ <h2 className="font-display text-4xl tracking-wide text-text-primary">NO ACTIVE SESSION</h2>
30
+ <p className="text-sm text-text-secondary">Start an agent session to see it here.</p>
31
+ </section>
32
+ );
33
+ }
34
+
35
+ const hasSkill = session.activeSkill !== undefined;
36
+ const skill = session.activeSkill || { name: 'NO SKILL', confidence: 'unknown' };
37
+ const lifecycle = session.lifecycle || 'starting';
38
+ const endedStatus = session.lifecycle === 'ended' ? session.status || 'ENDED' : undefined;
39
+
40
+ return (
41
+ <section className="panel p-6">
42
+ <div className={`absolute top-0 left-0 right-0 h-1 ${lifecycleColor(lifecycle)}`} />
43
+ <div className="flex flex-col gap-4">
44
+ <div className={`w-16 h-16 rounded-xl border border-border-default flex items-center justify-center ${hasSkill ? 'bg-elevated text-accent' : 'bg-warning/10 text-warning'}`}>
45
+ <Icon name={skillIcon(skill.name)} className="w-8 h-8" />
46
+ </div>
47
+ <h1 className="font-display text-6xl leading-none tracking-wide text-text-primary uppercase">
48
+ {skill.name}
49
+ </h1>
50
+ {!hasSkill && (
51
+ <p className="text-sm text-text-secondary">Agent is running without an active role.</p>
52
+ )}
53
+ <div className="flex flex-wrap gap-2">
54
+ <span className="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium border border-border-default bg-elevated text-text-secondary uppercase">
55
+ <span className={`w-2 h-2 rounded-full ${lifecycleColor(lifecycle)}`} />
56
+ {endedStatus || lifecycle}
57
+ </span>
58
+ <span className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-medium border border-border-default bg-elevated text-text-secondary uppercase">
59
+ {skill.confidence}
60
+ </span>
61
+ </div>
62
+ <div className="flex items-center gap-2 text-xs text-text-muted uppercase tracking-widest">
63
+ <Icon name={sourceIcon(session.source)} className="w-4 h-4" />
64
+ <span>{session.source}</span>
65
+ </div>
66
+ </div>
67
+ </section>
68
+ );
69
+ }
@@ -0,0 +1,74 @@
1
+ import { useEffect, useRef } from 'react';
2
+ import type { ClientSession } from '../../../src/types';
3
+
4
+ interface Props {
5
+ session: ClientSession | undefined;
6
+ }
7
+
8
+ export function ActivityGraph({ session }: Props) {
9
+ const canvasRef = useRef<HTMLCanvasElement>(null);
10
+
11
+ useEffect(() => {
12
+ const canvas = canvasRef.current;
13
+ if (!canvas) return;
14
+ const ctx = canvas.getContext('2d');
15
+ if (!ctx) return;
16
+
17
+ const rect = canvas.getBoundingClientRect();
18
+ const dpr = window.devicePixelRatio || 1;
19
+ canvas.width = Math.max(1, Math.floor(rect.width * dpr));
20
+ canvas.height = Math.max(1, Math.floor(rect.height * dpr));
21
+ ctx.scale(dpr, dpr);
22
+
23
+ const width = rect.width;
24
+ const height = rect.height;
25
+ const events = session?.events || [];
26
+
27
+ const now = Date.now();
28
+ const span = Math.max(60000, now - Math.min(...events.map((e) => e.timestamp), now));
29
+ const buckets = 40;
30
+ const bucketMs = span / buckets;
31
+ const counts = new Array(buckets).fill(0);
32
+
33
+ events.forEach((e) => {
34
+ const idx = Math.min(buckets - 1, Math.floor((now - e.timestamp) / bucketMs));
35
+ counts[buckets - 1 - idx]++;
36
+ });
37
+
38
+ const max = Math.max(1, ...counts);
39
+ const pad = 4;
40
+ const barW = (width - pad * 2) / buckets;
41
+
42
+ const style = getComputedStyle(document.documentElement);
43
+ const accent = style.getPropertyValue('--accent').trim() || '#f59e0b';
44
+ const inset = style.getPropertyValue('--bg-inset').trim() || '#050506';
45
+
46
+ ctx.fillStyle = inset;
47
+ ctx.fillRect(0, 0, width, height);
48
+
49
+ counts.forEach((count, i) => {
50
+ const barH = (count / max) * (height - pad * 2);
51
+ const x = pad + i * barW;
52
+ const y = height - pad - barH;
53
+ ctx.fillStyle = accent;
54
+ ctx.fillRect(x + 1, y, Math.max(1, barW - 2), Math.max(1, barH));
55
+ });
56
+ }, [session]);
57
+
58
+ return (
59
+ <section className="panel flex flex-col h-44">
60
+ <h2 className="text-xs font-medium text-text-muted uppercase tracking-widest px-5 py-4 border-b border-border-default">
61
+ Skill Activity
62
+ </h2>
63
+ <div className="flex-1 relative p-4">
64
+ {!session || session.events.length === 0 ? (
65
+ <div className="absolute inset-0 flex items-center justify-center text-sm text-text-muted">
66
+ Waiting for agent activity...
67
+ </div>
68
+ ) : (
69
+ <canvas ref={canvasRef} className="w-full h-full" />
70
+ )}
71
+ </div>
72
+ </section>
73
+ );
74
+ }
@@ -0,0 +1,200 @@
1
+ import { useCallback, useEffect, useMemo, useRef } from 'react';
2
+ import type { CommandPaletteItem } from '../lib/types';
3
+ import { search } from '../lib/search';
4
+ import { useCommandPalette } from '../hooks/useCommandPalette';
5
+ import { useSettings } from '../contexts/SettingsContext';
6
+ import { Icon } from './ui/Icon';
7
+
8
+ interface Props {
9
+ items: CommandPaletteItem[];
10
+ open: boolean;
11
+ onClose: () => void;
12
+ }
13
+
14
+ function groupByType(items: CommandPaletteItem[]) {
15
+ const groups: Record<string, CommandPaletteItem[]> = {};
16
+ for (const item of items) {
17
+ groups[item.type] = groups[item.type] || [];
18
+ groups[item.type].push(item);
19
+ }
20
+ return groups;
21
+ }
22
+
23
+ const GROUP_ORDER = ['view', 'session', 'skill', 'tool', 'file', 'event', 'action'];
24
+ const GROUP_LABELS: Record<string, string> = {
25
+ view: 'Views',
26
+ session: 'Sessions',
27
+ skill: 'Skills',
28
+ tool: 'Tools',
29
+ file: 'Files',
30
+ event: 'Recent events',
31
+ action: 'Actions',
32
+ };
33
+
34
+ export function CommandPalette({ items, open, onClose }: Props) {
35
+ const { query, setQuery, selectedIndex, setSelectedIndex } = useCommandPalette();
36
+ const { reducedMotion } = useSettings();
37
+ const inputRef = useRef<HTMLInputElement>(null);
38
+ const listRef = useRef<HTMLDivElement>(null);
39
+ const dialogRef = useRef<HTMLDivElement>(null);
40
+
41
+ const FOCUSABLE =
42
+ 'input, button, [href], select, textarea, [tabindex]:not([tabindex="-1"])';
43
+
44
+ const results = useMemo(() => search(items, query), [items, query]);
45
+ const flatResults = useMemo(() => {
46
+ const groups = groupByType(results);
47
+ const ordered: { item: CommandPaletteItem; group: string }[] = [];
48
+ for (const type of GROUP_ORDER) {
49
+ for (const item of groups[type] || []) ordered.push({ item, group: GROUP_LABELS[type] });
50
+ }
51
+ return ordered;
52
+ }, [results]);
53
+
54
+ useEffect(() => {
55
+ if (open) {
56
+ setQuery('');
57
+ inputRef.current?.focus();
58
+ }
59
+ }, [open, setQuery]);
60
+
61
+ useEffect(() => {
62
+ if (selectedIndex >= flatResults.length) setSelectedIndex(Math.max(0, flatResults.length - 1));
63
+ }, [flatResults.length, selectedIndex, setSelectedIndex]);
64
+
65
+ const activate = useCallback(
66
+ (index: number) => {
67
+ const entry = flatResults[index];
68
+ if (entry) {
69
+ entry.item.action();
70
+ onClose();
71
+ }
72
+ },
73
+ [flatResults, onClose]
74
+ );
75
+
76
+ useEffect(() => {
77
+ if (!open) return;
78
+ function onKeyDown(e: KeyboardEvent) {
79
+ if (e.key === 'Tab') {
80
+ const focusable = dialogRef.current?.querySelectorAll<HTMLElement>(FOCUSABLE);
81
+ if (!focusable || focusable.length === 0) return;
82
+ const current = document.activeElement as HTMLElement | null;
83
+ const idx = Array.from(focusable).indexOf(current as HTMLElement);
84
+ if (e.shiftKey) {
85
+ const prev = idx <= 0 ? focusable[focusable.length - 1] : focusable[idx - 1];
86
+ prev.focus();
87
+ } else {
88
+ const next = idx === -1 || idx >= focusable.length - 1 ? focusable[0] : focusable[idx + 1];
89
+ next.focus();
90
+ }
91
+ e.preventDefault();
92
+ return;
93
+ }
94
+ if (e.key === 'Escape') {
95
+ e.preventDefault();
96
+ onClose();
97
+ return;
98
+ }
99
+ if (flatResults.length === 0) return;
100
+ if (e.key === 'ArrowDown') {
101
+ e.preventDefault();
102
+ setSelectedIndex((i) => (i + 1) % flatResults.length);
103
+ return;
104
+ }
105
+ if (e.key === 'ArrowUp') {
106
+ e.preventDefault();
107
+ setSelectedIndex((i) => (i - 1 + flatResults.length) % flatResults.length);
108
+ return;
109
+ }
110
+ if (e.key === 'Enter') {
111
+ e.preventDefault();
112
+ activate(selectedIndex);
113
+ }
114
+ }
115
+ document.addEventListener('keydown', onKeyDown);
116
+ return () => document.removeEventListener('keydown', onKeyDown);
117
+ }, [open, flatResults.length, selectedIndex, activate, setSelectedIndex, onClose]);
118
+
119
+ useEffect(() => {
120
+ const active = listRef.current?.querySelector('[data-active="true"]') as HTMLElement | null;
121
+ active?.scrollIntoView({ block: 'nearest' });
122
+ }, [selectedIndex]);
123
+
124
+ if (!open) return null;
125
+
126
+ let currentGroup = '';
127
+
128
+ return (
129
+ <div className="fixed inset-0 z-[100] flex items-start justify-center pt-24">
130
+ <div
131
+ className="absolute inset-0 bg-black/60 backdrop-blur-sm"
132
+ onClick={onClose}
133
+ aria-hidden="true"
134
+ />
135
+ <div
136
+ ref={dialogRef}
137
+ role="dialog"
138
+ aria-modal="true"
139
+ aria-label="Command palette"
140
+ className={`relative w-full max-w-2xl mx-4 bg-surface border border-border-default rounded-xl shadow-2xl overflow-hidden ${
141
+ reducedMotion ? '' : 'animate-modal-in'
142
+ }`}
143
+ >
144
+ <div className="flex items-center gap-3 px-4 py-3 border-b border-border-default">
145
+ <Icon name="MagnifyingGlass" className="w-5 h-5 text-text-muted" />
146
+ <input
147
+ ref={inputRef}
148
+ type="text"
149
+ value={query}
150
+ onChange={(e) => setQuery(e.target.value)}
151
+ placeholder="Search views, sessions, skills, tools, files..."
152
+ className="flex-1 bg-transparent text-text-primary placeholder:text-text-muted outline-none text-sm"
153
+ />
154
+ <kbd className="hidden sm:inline-block px-1.5 py-0.5 rounded bg-base border border-border-default text-[10px] text-text-muted font-mono">
155
+ Esc
156
+ </kbd>
157
+ </div>
158
+ <div ref={listRef} className="max-h-[60vh] overflow-y-auto py-2">
159
+ {flatResults.length === 0 ? (
160
+ <div className="flex flex-col items-center justify-center gap-2 py-10 text-text-muted">
161
+ <Icon name="MagnifyingGlass" className="w-8 h-8" />
162
+ <p className="text-sm">No results found.</p>
163
+ </div>
164
+ ) : (
165
+ flatResults.map(({ item, group }, idx) => {
166
+ const showHeader = group !== currentGroup;
167
+ currentGroup = group;
168
+ const active = idx === selectedIndex;
169
+ return (
170
+ <div key={item.id}>
171
+ {showHeader && (
172
+ <div className="px-4 py-1.5 text-[11px] uppercase tracking-widest text-text-muted">
173
+ {group}
174
+ </div>
175
+ )}
176
+ <button
177
+ data-active={active}
178
+ onMouseEnter={() => setSelectedIndex(idx)}
179
+ onClick={() => activate(idx)}
180
+ className={`w-full flex items-center gap-3 px-4 py-2.5 text-left transition-colors ${
181
+ active ? 'bg-elevated' : 'hover:bg-elevated'
182
+ }`}
183
+ >
184
+ <Icon name={item.icon || 'Circle'} className="w-5 h-5 text-accent flex-shrink-0" />
185
+ <div className="flex flex-col min-w-0">
186
+ <span className="text-sm text-text-primary truncate">{item.title}</span>
187
+ {item.subtitle && (
188
+ <span className="text-xs text-text-muted truncate">{item.subtitle}</span>
189
+ )}
190
+ </div>
191
+ </button>
192
+ </div>
193
+ );
194
+ })
195
+ )}
196
+ </div>
197
+ </div>
198
+ </div>
199
+ );
200
+ }