@archznn/crewloop-skills 0.5.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 (250) hide show
  1. package/README.md +4 -16
  2. package/package.json +3 -3
  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 +31 -37
  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 +258 -98
  11. package/packages/cli/dist/hooks.js.map +1 -1
  12. package/packages/cli/dist/tests/cli.test.js +21 -0
  13. package/packages/cli/dist/tests/cli.test.js.map +1 -1
  14. package/packages/cli/dist/tests/hooks.test.js +253 -27
  15. package/packages/cli/dist/tests/hooks.test.js.map +1 -1
  16. package/references/conventions.md +1 -10
  17. package/references/workflow.md +1 -1
  18. package/servers/dashboard/README.md +55 -1
  19. package/servers/dashboard/bin/crewloop-shim.js +4 -0
  20. package/servers/dashboard/dist/adapters/agy.d.ts +19 -0
  21. package/servers/dashboard/dist/adapters/agy.d.ts.map +1 -0
  22. package/servers/dashboard/dist/adapters/agy.js +108 -0
  23. package/servers/dashboard/dist/adapters/agy.js.map +1 -0
  24. package/servers/dashboard/dist/adapters/codex.d.ts.map +1 -1
  25. package/servers/dashboard/dist/adapters/codex.js +2 -0
  26. package/servers/dashboard/dist/adapters/codex.js.map +1 -1
  27. package/servers/dashboard/dist/adapters/kimi.d.ts +1 -1
  28. package/servers/dashboard/dist/adapters/kimi.d.ts.map +1 -1
  29. package/servers/dashboard/dist/adapters/kimi.js +9 -0
  30. package/servers/dashboard/dist/adapters/kimi.js.map +1 -1
  31. package/servers/dashboard/dist/adapters/shim.d.ts +1 -1
  32. package/servers/dashboard/dist/adapters/shim.d.ts.map +1 -1
  33. package/servers/dashboard/dist/adapters/shim.js +32 -11
  34. package/servers/dashboard/dist/adapters/shim.js.map +1 -1
  35. package/servers/dashboard/dist/adapters/shim.test.js +46 -4
  36. package/servers/dashboard/dist/adapters/shim.test.js.map +1 -1
  37. package/servers/dashboard/dist/lib/constants.d.ts +5 -0
  38. package/servers/dashboard/dist/lib/constants.d.ts.map +1 -0
  39. package/servers/dashboard/dist/lib/constants.js +46 -0
  40. package/servers/dashboard/dist/lib/constants.js.map +1 -0
  41. package/servers/dashboard/dist/lib/format.d.ts +6 -0
  42. package/servers/dashboard/dist/lib/format.d.ts.map +1 -0
  43. package/servers/dashboard/dist/lib/format.js +52 -0
  44. package/servers/dashboard/dist/lib/format.js.map +1 -0
  45. package/servers/dashboard/dist/lib/graph.d.ts +22 -0
  46. package/servers/dashboard/dist/lib/graph.d.ts.map +1 -0
  47. package/servers/dashboard/dist/lib/graph.js +45 -0
  48. package/servers/dashboard/dist/lib/graph.js.map +1 -0
  49. package/servers/dashboard/dist/lib/invocations.d.ts +32 -0
  50. package/servers/dashboard/dist/lib/invocations.d.ts.map +1 -0
  51. package/servers/dashboard/dist/lib/invocations.js +135 -0
  52. package/servers/dashboard/dist/lib/invocations.js.map +1 -0
  53. package/servers/dashboard/dist/lib/invocations.test.d.ts +2 -0
  54. package/servers/dashboard/dist/lib/invocations.test.d.ts.map +1 -0
  55. package/servers/dashboard/dist/lib/invocations.test.js +68 -0
  56. package/servers/dashboard/dist/lib/invocations.test.js.map +1 -0
  57. package/servers/dashboard/dist/lib/paths.d.ts +2 -0
  58. package/servers/dashboard/dist/lib/paths.d.ts.map +1 -0
  59. package/servers/dashboard/dist/lib/paths.js +40 -0
  60. package/servers/dashboard/dist/lib/paths.js.map +1 -0
  61. package/servers/dashboard/dist/presenter.d.ts.map +1 -1
  62. package/servers/dashboard/dist/presenter.js +2 -0
  63. package/servers/dashboard/dist/presenter.js.map +1 -1
  64. package/servers/dashboard/dist/public/assets/index-DjmMKbPN.css +1 -0
  65. package/servers/dashboard/dist/public/assets/index-DzOqMleZ.js +5323 -0
  66. package/servers/dashboard/dist/public/assets/index-DzOqMleZ.js.map +1 -0
  67. package/servers/dashboard/dist/public/index.html +16 -0
  68. package/servers/dashboard/dist/server.d.ts.map +1 -1
  69. package/servers/dashboard/dist/server.js +5 -1
  70. package/servers/dashboard/dist/server.js.map +1 -1
  71. package/servers/dashboard/dist/skills/infer.d.ts.map +1 -1
  72. package/servers/dashboard/dist/skills/infer.js +0 -6
  73. package/servers/dashboard/dist/skills/infer.js.map +1 -1
  74. package/servers/dashboard/dist/skills/infer.test.js +10 -3
  75. package/servers/dashboard/dist/skills/infer.test.js.map +1 -1
  76. package/servers/dashboard/dist/skills/mapping.d.ts +0 -3
  77. package/servers/dashboard/dist/skills/mapping.d.ts.map +1 -1
  78. package/servers/dashboard/dist/skills/mapping.js +0 -18
  79. package/servers/dashboard/dist/skills/mapping.js.map +1 -1
  80. package/servers/dashboard/dist/skills/registry.d.ts.map +1 -1
  81. package/servers/dashboard/dist/skills/registry.js +0 -1
  82. package/servers/dashboard/dist/skills/registry.js.map +1 -1
  83. package/servers/dashboard/dist/tests/adapters.test.d.ts +2 -0
  84. package/servers/dashboard/dist/tests/adapters.test.d.ts.map +1 -0
  85. package/servers/dashboard/dist/tests/adapters.test.js +180 -0
  86. package/servers/dashboard/dist/tests/adapters.test.js.map +1 -0
  87. package/servers/dashboard/dist/tests/lib-helpers.test.d.ts +2 -0
  88. package/servers/dashboard/dist/tests/lib-helpers.test.d.ts.map +1 -0
  89. package/servers/dashboard/dist/tests/lib-helpers.test.js +123 -0
  90. package/servers/dashboard/dist/tests/lib-helpers.test.js.map +1 -0
  91. package/servers/dashboard/dist/tests/shim.test.d.ts +2 -0
  92. package/servers/dashboard/dist/tests/shim.test.d.ts.map +1 -0
  93. package/servers/dashboard/dist/tests/shim.test.js +133 -0
  94. package/servers/dashboard/dist/tests/shim.test.js.map +1 -0
  95. package/servers/dashboard/dist/types.d.ts +5 -2
  96. package/servers/dashboard/dist/types.d.ts.map +1 -1
  97. package/servers/dashboard/package.json +24 -6
  98. package/servers/dashboard/src/adapters/agy.ts +136 -0
  99. package/servers/dashboard/src/adapters/codex.ts +2 -0
  100. package/servers/dashboard/src/adapters/kimi.ts +11 -1
  101. package/servers/dashboard/src/adapters/shim.test.ts +57 -4
  102. package/servers/dashboard/src/adapters/shim.ts +31 -11
  103. package/servers/dashboard/src/lib/constants.ts +44 -0
  104. package/servers/dashboard/src/lib/format.ts +44 -0
  105. package/servers/dashboard/src/lib/graph.ts +69 -0
  106. package/servers/dashboard/src/lib/invocations.test.ts +70 -0
  107. package/servers/dashboard/src/lib/invocations.ts +172 -0
  108. package/servers/dashboard/src/lib/paths.ts +35 -0
  109. package/servers/dashboard/src/presenter.ts +2 -0
  110. package/servers/dashboard/src/server.ts +5 -1
  111. package/servers/dashboard/src/skills/infer.test.ts +11 -3
  112. package/servers/dashboard/src/skills/infer.ts +1 -8
  113. package/servers/dashboard/src/skills/mapping.ts +0 -20
  114. package/servers/dashboard/src/skills/registry.ts +0 -1
  115. package/servers/dashboard/src/tests/adapters.test.ts +198 -0
  116. package/servers/dashboard/src/tests/lib-helpers.test.ts +133 -0
  117. package/servers/dashboard/src/tests/shim.test.ts +153 -0
  118. package/servers/dashboard/src/types.ts +5 -3
  119. package/servers/dashboard/ui/index.html +15 -0
  120. package/servers/dashboard/ui/postcss.config.js +6 -0
  121. package/servers/dashboard/ui/src/App.tsx +360 -0
  122. package/servers/dashboard/ui/src/components/ActiveSkillPanel.tsx +69 -0
  123. package/servers/dashboard/ui/src/components/ActivityGraph.tsx +74 -0
  124. package/servers/dashboard/ui/src/components/CommandPalette.tsx +200 -0
  125. package/servers/dashboard/ui/src/components/FileActivity.tsx +20 -0
  126. package/servers/dashboard/ui/src/components/FileDiff.tsx +68 -0
  127. package/servers/dashboard/ui/src/components/FileList.tsx +64 -0
  128. package/servers/dashboard/ui/src/components/FilterBar.tsx +208 -0
  129. package/servers/dashboard/ui/src/components/Network3D.tsx +178 -0
  130. package/servers/dashboard/ui/src/components/SessionSelector.tsx +95 -0
  131. package/servers/dashboard/ui/src/components/Sidebar.tsx +110 -0
  132. package/servers/dashboard/ui/src/components/TelemetryPanel.tsx +57 -0
  133. package/servers/dashboard/ui/src/components/Timeline.tsx +57 -0
  134. package/servers/dashboard/ui/src/components/TimelineRow.tsx +112 -0
  135. package/servers/dashboard/ui/src/components/TopBar.tsx +116 -0
  136. package/servers/dashboard/ui/src/components/ViewHeader.tsx +19 -0
  137. package/servers/dashboard/ui/src/components/ui/Icon.tsx +105 -0
  138. package/servers/dashboard/ui/src/components/ui/StatusBadge.tsx +19 -0
  139. package/servers/dashboard/ui/src/components/views/FilesView.tsx +23 -0
  140. package/servers/dashboard/ui/src/components/views/NetworkView.tsx +20 -0
  141. package/servers/dashboard/ui/src/components/views/Overview.tsx +135 -0
  142. package/servers/dashboard/ui/src/components/views/SessionsView.tsx +84 -0
  143. package/servers/dashboard/ui/src/components/views/SettingsView.tsx +138 -0
  144. package/servers/dashboard/ui/src/components/views/SkillsView.tsx +92 -0
  145. package/servers/dashboard/ui/src/components/views/TimelineView.tsx +46 -0
  146. package/servers/dashboard/ui/src/contexts/FilterContext.tsx +41 -0
  147. package/servers/dashboard/ui/src/contexts/PinnedSessionsContext.tsx +80 -0
  148. package/servers/dashboard/ui/src/contexts/SettingsContext.tsx +60 -0
  149. package/servers/dashboard/ui/src/hooks/useCommandPalette.ts +36 -0
  150. package/servers/dashboard/ui/src/hooks/useKeyboardShortcut.ts +38 -0
  151. package/servers/dashboard/ui/src/hooks/useNow.ts +12 -0
  152. package/servers/dashboard/ui/src/hooks/useReducedMotion.ts +15 -0
  153. package/servers/dashboard/ui/src/hooks/useSessions.ts +64 -0
  154. package/servers/dashboard/ui/src/hooks/useTheme.ts +30 -0
  155. package/servers/dashboard/ui/src/hooks/useViewport.ts +19 -0
  156. package/servers/dashboard/ui/src/hooks/useWebSocket.ts +118 -0
  157. package/servers/dashboard/ui/src/lib/export.test.ts +33 -0
  158. package/servers/dashboard/ui/src/lib/export.ts +39 -0
  159. package/servers/dashboard/ui/src/lib/filter.test.ts +95 -0
  160. package/servers/dashboard/ui/src/lib/filter.ts +178 -0
  161. package/servers/dashboard/ui/src/lib/format.test.ts +25 -0
  162. package/servers/dashboard/ui/src/lib/search.test.ts +52 -0
  163. package/servers/dashboard/ui/src/lib/search.ts +60 -0
  164. package/servers/dashboard/ui/src/lib/settings.test.ts +50 -0
  165. package/servers/dashboard/ui/src/lib/settings.ts +56 -0
  166. package/servers/dashboard/ui/src/lib/types.ts +124 -0
  167. package/servers/dashboard/ui/src/main.tsx +19 -0
  168. package/servers/dashboard/ui/src/styles/index.css +155 -0
  169. package/servers/dashboard/ui/tailwind.config.js +45 -0
  170. package/servers/dashboard/ui/tsconfig.json +33 -0
  171. package/servers/dashboard/ui/tsconfig.node.json +10 -0
  172. package/servers/dashboard/ui/vite.config.ts +37 -0
  173. package/servers/dashboard/ui/vitest.config.ts +8 -0
  174. package/skills/accessibility-auditor/SKILL.md +0 -20
  175. package/skills/architect/SKILL.md +0 -45
  176. package/skills/designer/SKILL.md +0 -30
  177. package/skills/docs-writer/SKILL.md +0 -13
  178. package/skills/engineer/SKILL.md +0 -30
  179. package/skills/maintainer/SKILL.md +0 -20
  180. package/skills/orchestrator/SKILL.md +0 -13
  181. package/skills/product-manager/SKILL.md +0 -20
  182. package/skills/researcher/SKILL.md +0 -20
  183. package/skills/reviewer/SKILL.md +0 -30
  184. package/skills/security-guard/SKILL.md +0 -20
  185. package/skills/shipper/SKILL.md +0 -33
  186. package/skills/tester/SKILL.md +0 -20
  187. package/packages/cli/dist/mcp.d.ts +0 -28
  188. package/packages/cli/dist/mcp.d.ts.map +0 -1
  189. package/packages/cli/dist/mcp.js +0 -148
  190. package/packages/cli/dist/mcp.js.map +0 -1
  191. package/packages/cli/dist/tests/mcp.test.d.ts +0 -2
  192. package/packages/cli/dist/tests/mcp.test.d.ts.map +0 -1
  193. package/packages/cli/dist/tests/mcp.test.js +0 -232
  194. package/packages/cli/dist/tests/mcp.test.js.map +0 -1
  195. package/references/obsidian-mcp-usage.md +0 -190
  196. package/servers/dashboard/public/app.js +0 -516
  197. package/servers/dashboard/public/index.html +0 -96
  198. package/servers/dashboard/public/styles.css +0 -819
  199. package/servers/obsidian-mcp/README.md +0 -82
  200. package/servers/obsidian-mcp/pyproject.toml +0 -32
  201. package/servers/obsidian-mcp/src/obsidian_mcp/__init__.py +0 -0
  202. package/servers/obsidian-mcp/src/obsidian_mcp/config.py +0 -47
  203. package/servers/obsidian-mcp/src/obsidian_mcp/indexer/__init__.py +0 -0
  204. package/servers/obsidian-mcp/src/obsidian_mcp/indexer/embeddings.py +0 -105
  205. package/servers/obsidian-mcp/src/obsidian_mcp/indexer/indexer.py +0 -79
  206. package/servers/obsidian-mcp/src/obsidian_mcp/indexer/store.py +0 -141
  207. package/servers/obsidian-mcp/src/obsidian_mcp/indexer/sync.py +0 -37
  208. package/servers/obsidian-mcp/src/obsidian_mcp/learning/__init__.py +0 -0
  209. package/servers/obsidian-mcp/src/obsidian_mcp/learning/detector.py +0 -66
  210. package/servers/obsidian-mcp/src/obsidian_mcp/learning/note_generator.py +0 -40
  211. package/servers/obsidian-mcp/src/obsidian_mcp/main.py +0 -4
  212. package/servers/obsidian-mcp/src/obsidian_mcp/models.py +0 -42
  213. package/servers/obsidian-mcp/src/obsidian_mcp/privacy/__init__.py +0 -0
  214. package/servers/obsidian-mcp/src/obsidian_mcp/privacy/filter.py +0 -68
  215. package/servers/obsidian-mcp/src/obsidian_mcp/rag/__init__.py +0 -0
  216. package/servers/obsidian-mcp/src/obsidian_mcp/rag/engine.py +0 -50
  217. package/servers/obsidian-mcp/src/obsidian_mcp/rag/graph_search.py +0 -55
  218. package/servers/obsidian-mcp/src/obsidian_mcp/rag/text_search.py +0 -37
  219. package/servers/obsidian-mcp/src/obsidian_mcp/rag/vector_search.py +0 -118
  220. package/servers/obsidian-mcp/src/obsidian_mcp/server.py +0 -61
  221. package/servers/obsidian-mcp/src/obsidian_mcp/tools/__init__.py +0 -0
  222. package/servers/obsidian-mcp/src/obsidian_mcp/tools/create.py +0 -43
  223. package/servers/obsidian-mcp/src/obsidian_mcp/tools/delete.py +0 -16
  224. package/servers/obsidian-mcp/src/obsidian_mcp/tools/learn.py +0 -42
  225. package/servers/obsidian-mcp/src/obsidian_mcp/tools/list.py +0 -16
  226. package/servers/obsidian-mcp/src/obsidian_mcp/tools/read.py +0 -15
  227. package/servers/obsidian-mcp/src/obsidian_mcp/tools/registry.py +0 -130
  228. package/servers/obsidian-mcp/src/obsidian_mcp/tools/related.py +0 -20
  229. package/servers/obsidian-mcp/src/obsidian_mcp/tools/search.py +0 -26
  230. package/servers/obsidian-mcp/src/obsidian_mcp/tools/sync.py +0 -22
  231. package/servers/obsidian-mcp/src/obsidian_mcp/tools/update.py +0 -34
  232. package/servers/obsidian-mcp/src/obsidian_mcp/vault/__init__.py +0 -0
  233. package/servers/obsidian-mcp/src/obsidian_mcp/vault/parser.py +0 -82
  234. package/servers/obsidian-mcp/src/obsidian_mcp/vault/repository.py +0 -68
  235. package/servers/obsidian-mcp/src/obsidian_mcp/vault/writer.py +0 -61
  236. package/servers/obsidian-mcp/tests/conftest.py +0 -39
  237. package/servers/obsidian-mcp/tests/test_async_tools.py +0 -87
  238. package/servers/obsidian-mcp/tests/test_edge_cases.py +0 -59
  239. package/servers/obsidian-mcp/tests/test_indexer.py +0 -27
  240. package/servers/obsidian-mcp/tests/test_integration.py +0 -90
  241. package/servers/obsidian-mcp/tests/test_learning.py +0 -34
  242. package/servers/obsidian-mcp/tests/test_privacy.py +0 -31
  243. package/servers/obsidian-mcp/tests/test_privacy_config.py +0 -44
  244. package/servers/obsidian-mcp/tests/test_rag.py +0 -64
  245. package/servers/obsidian-mcp/tests/test_read_raw.py +0 -37
  246. package/servers/obsidian-mcp/tests/test_tfidf_fallback.py +0 -54
  247. package/servers/obsidian-mcp/tests/test_tools.py +0 -108
  248. package/servers/obsidian-mcp/tests/test_vault.py +0 -103
  249. package/servers/obsidian-mcp/tests/test_writer.py +0 -139
  250. package/skills/obsidian-second-brain/SKILL.md +0 -298
@@ -0,0 +1,110 @@
1
+ import type { View } from '../lib/types';
2
+ import { useSettings } from '../contexts/SettingsContext';
3
+ import { useViewport } from '../hooks/useViewport';
4
+ import { Icon } from './ui/Icon';
5
+
6
+ interface NavItem {
7
+ key: View;
8
+ label: string;
9
+ icon: string;
10
+ }
11
+
12
+ const ITEMS: NavItem[] = [
13
+ { key: 'overview', label: 'Overview', icon: 'House' },
14
+ { key: 'sessions', label: 'Sessions', icon: 'Rows' },
15
+ { key: 'timeline', label: 'Timeline', icon: 'Clock' },
16
+ { key: 'network', label: 'Network', icon: 'Graph' },
17
+ { key: 'files', label: 'Files', icon: 'Files' },
18
+ { key: 'skills', label: 'Skills', icon: 'ChartPie' },
19
+ ];
20
+
21
+ const SETTINGS_ITEM: NavItem = { key: 'settings', label: 'Settings', icon: 'Gear' };
22
+
23
+ interface Props {
24
+ activeView: View;
25
+ onChange: (view: View) => void;
26
+ mobileOpen: boolean;
27
+ onClose: () => void;
28
+ }
29
+
30
+ export function Sidebar({ activeView, onChange, mobileOpen, onClose }: Props) {
31
+ const { breakpoint } = useViewport();
32
+ const { reducedMotion } = useSettings();
33
+ const isMobile = breakpoint === 'mobile';
34
+ const isTablet = breakpoint === 'tablet';
35
+
36
+ function handleSelect(view: View) {
37
+ onChange(view);
38
+ if (isMobile) onClose();
39
+ }
40
+
41
+ function renderItem(item: NavItem) {
42
+ const active = activeView === item.key;
43
+ return (
44
+ <button
45
+ key={item.key}
46
+ onClick={() => handleSelect(item.key)}
47
+ aria-current={active ? 'page' : undefined}
48
+ className={`sidebar-item group relative flex items-center gap-3 w-full rounded-lg transition-colors text-left ${
49
+ isTablet ? 'justify-center h-11' : 'h-10 px-3'
50
+ } ${
51
+ active
52
+ ? 'bg-elevated text-text-primary'
53
+ : 'text-text-secondary hover:bg-elevated hover:text-text-primary'
54
+ }`}
55
+ >
56
+ {active && !reducedMotion && (
57
+ <span className="absolute left-0 top-1.5 bottom-1.5 w-[3px] rounded-r bg-accent" />
58
+ )}
59
+ {active && reducedMotion && (
60
+ <span className="absolute left-0 top-1.5 bottom-1.5 w-[3px] rounded-r bg-accent" />
61
+ )}
62
+ <Icon name={item.icon} className="w-5 h-5 flex-shrink-0" />
63
+ {!isTablet && <span className="text-sm font-medium">{item.label}</span>}
64
+ </button>
65
+ );
66
+ }
67
+
68
+ const content = (
69
+ <nav aria-label="Main" className="flex flex-col h-full py-4 px-3 gap-2">
70
+ <div className="flex flex-col gap-1">
71
+ {ITEMS.map(renderItem)}
72
+ </div>
73
+ <div className="flex-1" />
74
+ <div className="border-t border-border-default pt-2">
75
+ {renderItem(SETTINGS_ITEM)}
76
+ </div>
77
+ </nav>
78
+ );
79
+
80
+ if (isMobile) {
81
+ return (
82
+ <>
83
+ {mobileOpen && (
84
+ <div
85
+ className="fixed inset-0 bg-black/60 z-40"
86
+ onClick={onClose}
87
+ aria-hidden="true"
88
+ />
89
+ )}
90
+ <aside
91
+ className={`fixed top-14 left-0 bottom-0 w-60 bg-surface border-r border-border-default z-40 transform transition-transform ${
92
+ mobileOpen ? 'translate-x-0' : '-translate-x-full'
93
+ }`}
94
+ >
95
+ {content}
96
+ </aside>
97
+ </>
98
+ );
99
+ }
100
+
101
+ return (
102
+ <aside
103
+ className={`flex-shrink-0 h-[calc(100vh-3.5rem)] bg-surface border-r border-border-default sticky top-14 ${
104
+ isTablet ? 'w-16' : 'w-60'
105
+ }`}
106
+ >
107
+ {content}
108
+ </aside>
109
+ );
110
+ }
@@ -0,0 +1,57 @@
1
+ import { useEffect, useMemo, useState } from 'react';
2
+ import type { ClientSession } from '../../../src/types';
3
+ import { formatDuration } from '../../../src/lib/format';
4
+
5
+ interface Props {
6
+ session: ClientSession | undefined;
7
+ }
8
+
9
+ export function TelemetryPanel({ session }: Props) {
10
+ const [now, setNow] = useState(Date.now());
11
+
12
+ useEffect(() => {
13
+ const id = setInterval(() => setNow(Date.now()), 1000);
14
+ return () => clearInterval(id);
15
+ }, []);
16
+
17
+ const toolEvents = useMemo(
18
+ () => (session?.events || []).filter((e) => e.event_type === 'tool_start' || e.event_type === 'tool_end'),
19
+ [session]
20
+ );
21
+
22
+ const eventRate = useMemo(() => {
23
+ const windowStart = now - 60000;
24
+ return (session?.events || []).filter((e) => e.timestamp > windowStart).length;
25
+ }, [session, now]);
26
+
27
+ const duration = useMemo(() => {
28
+ if (!session) return 0;
29
+ const end = session.endedAt || session.lastActivity || now;
30
+ return end - session.startTime;
31
+ }, [session, now]);
32
+
33
+ const cards = [
34
+ { label: 'Tools', value: Math.ceil(toolEvents.length / 2) },
35
+ { label: 'Duration', value: formatDuration(duration) },
36
+ { label: 'Rate/m', value: eventRate },
37
+ ];
38
+
39
+ return (
40
+ <section className="panel p-5">
41
+ <h2 className="text-xs font-medium text-text-muted uppercase tracking-widest pb-4 border-b border-border-default">
42
+ Telemetry
43
+ </h2>
44
+ <div className="pt-4 flex flex-col gap-4">
45
+ {cards.map((c) => (
46
+ <div
47
+ key={c.label}
48
+ className="flex flex-col gap-1 p-3 bg-inset border border-border-default rounded"
49
+ >
50
+ <span className="text-2xl font-medium tabular text-accent">{c.value}</span>
51
+ <span className="text-xs text-text-muted uppercase tracking-widest">{c.label}</span>
52
+ </div>
53
+ ))}
54
+ </div>
55
+ </section>
56
+ );
57
+ }
@@ -0,0 +1,57 @@
1
+ import type { ToolInvocation } from '../../../src/lib/invocations';
2
+ import { TimelineRow } from './TimelineRow';
3
+
4
+ interface Props {
5
+ invocations: ToolInvocation[];
6
+ expandedIds: Set<string>;
7
+ onToggle: (id: string) => void;
8
+ onMouseEnter?: () => void;
9
+ onMouseLeave?: () => void;
10
+ }
11
+
12
+ export function Timeline({ invocations, expandedIds, onToggle, onMouseEnter, onMouseLeave }: Props) {
13
+ async function handleCopy(inv: ToolInvocation) {
14
+ const text = JSON.stringify(
15
+ {
16
+ id: inv.id,
17
+ tool: inv.tool,
18
+ status: inv.status,
19
+ startTime: inv.startTime,
20
+ durationMs: inv.durationMs,
21
+ skill: inv.skill,
22
+ detail: inv.detail,
23
+ },
24
+ null,
25
+ 2
26
+ );
27
+ try {
28
+ await navigator.clipboard.writeText(text);
29
+ } catch {
30
+ // Ignore clipboard errors.
31
+ }
32
+ }
33
+
34
+ return (
35
+ <div
36
+ className="flex-1 overflow-y-auto px-5 py-3"
37
+ onMouseEnter={onMouseEnter}
38
+ onMouseLeave={onMouseLeave}
39
+ >
40
+ {invocations.length === 0 ? (
41
+ <p className="text-center py-8 text-text-muted text-sm">No events match the filters.</p>
42
+ ) : (
43
+ <ul className="relative list-none m-0 p-0 before:content-[''] before:absolute before:top-3 before:bottom-3 before:left-[32px] before:w-px before:bg-border-default">
44
+ {invocations.map((inv) => (
45
+ <TimelineRow
46
+ key={inv.id}
47
+ inv={inv}
48
+ expanded={expandedIds.has(inv.id)}
49
+ onToggle={() => onToggle(inv.id)}
50
+ onCopy={() => handleCopy(inv)}
51
+ />
52
+ ))}
53
+ </ul>
54
+ )}
55
+ </div>
56
+ );
57
+ }
@@ -0,0 +1,112 @@
1
+ import { useMemo, useState } from 'react';
2
+ import type { ToolInvocation } from '../../../src/lib/invocations';
3
+ import { formatDuration, formatTime, escapeHtml, prettyJson } from '../../../src/lib/format';
4
+ import { Icon } from './ui/Icon';
5
+
6
+ interface Props {
7
+ inv: ToolInvocation;
8
+ expanded: boolean;
9
+ onToggle: () => void;
10
+ onCopy: () => void;
11
+ }
12
+
13
+ function statusClasses(status: string): { row: string; dot: string; icon: string } {
14
+ switch (status) {
15
+ case 'running':
16
+ return {
17
+ row: 'bg-running/10 border-l-running',
18
+ dot: 'bg-running animate-pulse',
19
+ icon: 'Spinner',
20
+ };
21
+ case 'success':
22
+ return {
23
+ row: 'bg-success/5 border-l-success',
24
+ dot: 'bg-success',
25
+ icon: 'Check',
26
+ };
27
+ case 'error':
28
+ return {
29
+ row: 'bg-error/5 border-l-error',
30
+ dot: 'bg-error',
31
+ icon: 'X',
32
+ };
33
+ default:
34
+ return {
35
+ row: 'border-l-transparent opacity-90',
36
+ dot: 'bg-text-muted',
37
+ icon: '',
38
+ };
39
+ }
40
+ }
41
+
42
+ function Payload({ label, payload }: { label: string; payload?: Record<string, unknown> }) {
43
+ if (!payload || Object.keys(payload).length === 0) return null;
44
+ return (
45
+ <div className="flex flex-col gap-1.5">
46
+ <span className="text-[11px] font-semibold text-text-muted uppercase tracking-widest">{label}</span>
47
+ <pre className="p-2.5 bg-base border border-border-default rounded text-xs text-text-secondary whitespace-pre-wrap break-words max-h-52 overflow-auto">
48
+ <code dangerouslySetInnerHTML={{ __html: escapeHtml(prettyJson(payload)) }} />
49
+ </pre>
50
+ </div>
51
+ );
52
+ }
53
+
54
+ export function TimelineRow({ inv, expanded, onToggle, onCopy }: Props) {
55
+ const status = statusClasses(inv.status);
56
+ const [copied, setCopied] = useState(false);
57
+ const hasDetails = useMemo(
58
+ () => (inv.input && Object.keys(inv.input).length > 0) || (inv.output && Object.keys(inv.output).length > 0),
59
+ [inv.input, inv.output]
60
+ );
61
+
62
+ async function handleCopy(e: React.MouseEvent) {
63
+ e.stopPropagation();
64
+ await onCopy();
65
+ setCopied(true);
66
+ window.setTimeout(() => setCopied(false), 1200);
67
+ }
68
+
69
+ return (
70
+ <li
71
+ onClick={onToggle}
72
+ className={`timeline-row grid grid-cols-[56px_16px_1fr_auto] items-start gap-3 py-2 px-2.5 -mx-2.5 rounded cursor-pointer border-l-2 hover:bg-elevated transition-colors ${status.row}`}
73
+ role="listitem"
74
+ >
75
+ <span className="text-xs text-text-muted text-right pt-0.5 tabular">{formatTime(inv.startTime)}</span>
76
+ <span className={`w-2 h-2 rounded-full justify-self-center mt-1.5 z-10 ${status.dot}`} />
77
+ <div className="flex items-baseline gap-2.5 min-w-0 flex-wrap">
78
+ <span className="font-mono text-sm font-semibold text-text-primary flex-shrink-0">{inv.tool}</span>
79
+ <span className="text-sm text-text-secondary truncate min-w-0">
80
+ {inv.detail || inv.skill || ''}
81
+ </span>
82
+ {inv.durationMs ? (
83
+ <span className="text-[11px] text-text-muted bg-inset px-1.5 py-0.5 rounded ml-auto tabular">
84
+ {formatDuration(inv.durationMs)}
85
+ </span>
86
+ ) : null}
87
+ </div>
88
+ <div className="flex items-center gap-1 text-text-muted mt-0.5">
89
+ {status.icon ? <Icon name={status.icon} className={`w-4 h-4 ${status.icon === 'Spinner' ? 'animate-spin' : ''}`} /> : null}
90
+ <button
91
+ onClick={handleCopy}
92
+ aria-label="Copy event"
93
+ className="p-1 rounded hover:bg-base hover:text-accent transition-colors"
94
+ >
95
+ <Icon name={copied ? 'Check' : 'Copy'} className="w-3.5 h-3.5" />
96
+ </button>
97
+ </div>
98
+ {expanded && (
99
+ <div className="col-span-3 col-start-2 mt-2 p-3 bg-inset border border-border-default rounded flex flex-col gap-3">
100
+ {hasDetails ? (
101
+ <>
102
+ <Payload label="Input" payload={inv.input} />
103
+ <Payload label="Output" payload={inv.output} />
104
+ </>
105
+ ) : (
106
+ <p className="text-sm text-text-muted">No details available.</p>
107
+ )}
108
+ </div>
109
+ )}
110
+ </li>
111
+ );
112
+ }
@@ -0,0 +1,116 @@
1
+ import type { ClientSession } from '../../../src/types';
2
+ import type { View } from '../lib/types';
3
+ import { useSettings } from '../contexts/SettingsContext';
4
+ import { SessionSelector } from './SessionSelector';
5
+ import { Icon } from './ui/Icon';
6
+
7
+ interface Props {
8
+ activeView: View;
9
+ sessions: ClientSession[];
10
+ selectedSessionId: string | null;
11
+ activeSessionId: string | undefined;
12
+ connection: 'connecting' | 'connected' | 'disconnected';
13
+ onSelectSession: (id: string) => void;
14
+ onOpenCommandPalette: () => void;
15
+ onToggleSidebar: () => void;
16
+ }
17
+
18
+ const VIEW_TITLES: Record<View, string> = {
19
+ overview: 'Overview',
20
+ sessions: 'Sessions',
21
+ timeline: 'Timeline',
22
+ network: 'Network',
23
+ files: 'Files',
24
+ skills: 'Skills',
25
+ settings: 'Settings',
26
+ };
27
+
28
+ function commandHint(): string {
29
+ if (typeof navigator !== 'undefined' && /Mac|iPod|iPhone|iPad/.test(navigator.platform)) {
30
+ return '⌘K';
31
+ }
32
+ return 'Ctrl+K';
33
+ }
34
+
35
+ export function TopBar({
36
+ activeView,
37
+ sessions,
38
+ selectedSessionId,
39
+ activeSessionId,
40
+ connection,
41
+ onSelectSession,
42
+ onOpenCommandPalette,
43
+ onToggleSidebar,
44
+ }: Props) {
45
+ const { resolvedTheme, setSettings } = useSettings();
46
+
47
+ return (
48
+ <header className="h-14 bg-surface border-b border-border-default flex items-center justify-between px-4 flex-shrink-0 z-50">
49
+ <div className="flex items-center gap-3">
50
+ <button
51
+ onClick={onToggleSidebar}
52
+ aria-label="Toggle sidebar"
53
+ className="lg:hidden w-9 h-9 rounded-lg border border-border-default bg-elevated text-text-secondary hover:border-accent hover:text-accent transition-colors flex items-center justify-center"
54
+ >
55
+ <Icon name="List" className="w-5 h-5" />
56
+ </button>
57
+ <div className="flex items-baseline gap-2">
58
+ <span className="font-display text-2xl tracking-widest text-text-primary">CREWLOOP</span>
59
+ <span className="text-text-muted hidden sm:inline">·</span>
60
+ <span className="text-xs text-text-muted tracking-widest uppercase hidden sm:inline">
61
+ {VIEW_TITLES[activeView]}
62
+ </span>
63
+ </div>
64
+ </div>
65
+
66
+ <div className="flex items-center gap-2">
67
+ <button
68
+ onClick={onOpenCommandPalette}
69
+ className="hidden md:flex items-center gap-2 px-3 py-1.5 rounded-lg border border-border-default bg-elevated text-text-secondary hover:border-accent hover:text-accent transition-colors text-xs"
70
+ >
71
+ <Icon name="MagnifyingGlass" className="w-4 h-4" />
72
+ <span>Search</span>
73
+ <kbd className="ml-2 px-1.5 py-0.5 rounded bg-base border border-border-default text-[10px] font-mono">
74
+ {commandHint()}
75
+ </kbd>
76
+ </button>
77
+ <button
78
+ onClick={onOpenCommandPalette}
79
+ aria-label="Search"
80
+ className="md:hidden w-9 h-9 rounded-lg border border-border-default bg-elevated text-text-secondary hover:border-accent hover:text-accent transition-colors flex items-center justify-center"
81
+ >
82
+ <Icon name="MagnifyingGlass" className="w-5 h-5" />
83
+ </button>
84
+
85
+ <div className="hidden sm:flex items-center gap-1.5 px-2 py-1 rounded-lg border border-border-default bg-elevated text-xs text-text-secondary">
86
+ <span
87
+ className={`w-2 h-2 rounded-full ${
88
+ connection === 'connected'
89
+ ? 'bg-success'
90
+ : connection === 'connecting'
91
+ ? 'bg-warning'
92
+ : 'bg-error'
93
+ }`}
94
+ />
95
+ <span className="capitalize">{connection}</span>
96
+ </div>
97
+
98
+ <button
99
+ onClick={() => setSettings((s) => ({ ...s, theme: resolvedTheme === 'dark' ? 'light' : 'dark' }))}
100
+ aria-label="Toggle theme"
101
+ className="w-9 h-9 rounded-lg border border-border-default bg-elevated text-text-secondary hover:border-accent hover:text-accent transition-colors flex items-center justify-center"
102
+ >
103
+ <Icon name={resolvedTheme === 'light' ? 'Sun' : 'Moon'} className="w-5 h-5" />
104
+ </button>
105
+
106
+ <SessionSelector
107
+ sessions={sessions}
108
+ selectedSessionId={selectedSessionId}
109
+ activeSessionId={activeSessionId}
110
+ connection={connection}
111
+ onSelect={onSelectSession}
112
+ />
113
+ </div>
114
+ </header>
115
+ );
116
+ }
@@ -0,0 +1,19 @@
1
+ import { Icon } from './ui/Icon';
2
+
3
+ interface Props {
4
+ title: string;
5
+ icon?: string;
6
+ children?: React.ReactNode;
7
+ }
8
+
9
+ export function ViewHeader({ title, icon, children }: Props) {
10
+ return (
11
+ <div className="flex items-center justify-between px-5 py-4 border-b border-border-default flex-shrink-0">
12
+ <div className="flex items-center gap-2">
13
+ {icon && <Icon name={icon} className="w-5 h-5 text-accent" />}
14
+ <h1 className="font-display text-3xl tracking-wide text-text-primary uppercase">{title}</h1>
15
+ </div>
16
+ {children && <div className="flex items-center gap-2">{children}</div>}
17
+ </div>
18
+ );
19
+ }
@@ -0,0 +1,105 @@
1
+ import type { IconProps } from '@phosphor-icons/react';
2
+ import {
3
+ Target,
4
+ Blueprint,
5
+ Palette,
6
+ Wrench,
7
+ MagnifyingGlass,
8
+ RocketLaunch,
9
+ Article,
10
+ Flask,
11
+ ChartBar,
12
+ Toolbox,
13
+ Microscope,
14
+ Shield,
15
+ Person,
16
+ Brain,
17
+ TreeStructure,
18
+ Circle,
19
+ ChatTeardropText,
20
+ Terminal,
21
+ Planet,
22
+ CodeBlock,
23
+ FileText,
24
+ Monitor,
25
+ Moon,
26
+ Sun,
27
+ CaretDown,
28
+ MonitorPlay,
29
+ Check,
30
+ X,
31
+ Spinner,
32
+ House,
33
+ Rows,
34
+ Clock,
35
+ Graph,
36
+ Files,
37
+ ChartPie,
38
+ Gear,
39
+ ArrowsInLineVertical,
40
+ ArrowsOutLineVertical,
41
+ PushPin,
42
+ PushPinSlash,
43
+ DownloadSimple,
44
+ Copy,
45
+ XCircle,
46
+ Faders,
47
+ List,
48
+ } from '@phosphor-icons/react';
49
+
50
+ const ICON_MAP: Record<string, React.ComponentType<IconProps>> = {
51
+ Target,
52
+ Blueprint,
53
+ Palette,
54
+ Wrench,
55
+ MagnifyingGlass,
56
+ RocketLaunch,
57
+ Article,
58
+ Flask,
59
+ ChartBar,
60
+ Toolbox,
61
+ Microscope,
62
+ Shield,
63
+ Person,
64
+ Brain,
65
+ TreeStructure,
66
+ Circle,
67
+ ChatTeardropText,
68
+ Terminal,
69
+ Planet,
70
+ CodeBlock,
71
+ FileText,
72
+ Monitor,
73
+ Moon,
74
+ Sun,
75
+ CaretDown,
76
+ MonitorPlay,
77
+ Check,
78
+ X,
79
+ Spinner,
80
+ House,
81
+ Rows,
82
+ Clock,
83
+ Graph,
84
+ Files,
85
+ ChartPie,
86
+ Gear,
87
+ ArrowsInLineVertical,
88
+ ArrowsOutLineVertical,
89
+ PushPin,
90
+ PushPinSlash,
91
+ DownloadSimple,
92
+ Copy,
93
+ XCircle,
94
+ Faders,
95
+ List,
96
+ };
97
+
98
+ interface Props extends Omit<IconProps, 'ref'> {
99
+ name: string;
100
+ }
101
+
102
+ export function Icon({ name, ...props }: Props) {
103
+ const Comp = ICON_MAP[name] || Circle;
104
+ return <Comp {...props} />;
105
+ }
@@ -0,0 +1,19 @@
1
+ interface Props {
2
+ status: string;
3
+ }
4
+
5
+ export function StatusBadge({ status }: Props) {
6
+ const color =
7
+ status === 'running'
8
+ ? 'text-running border-running/35'
9
+ : status === 'success'
10
+ ? 'text-success border-success/35'
11
+ : status === 'error'
12
+ ? 'text-error border-error/35'
13
+ : 'text-text-muted border-border-default';
14
+ return (
15
+ <span className={`text-[10px] font-semibold uppercase px-1.5 py-0.5 rounded border ${color}`}>
16
+ {status}
17
+ </span>
18
+ );
19
+ }
@@ -0,0 +1,23 @@
1
+ import { useState } from 'react';
2
+ import type { FileEntry } from '../../../../src/lib/invocations';
3
+ import { ViewHeader } from '../ViewHeader';
4
+ import { FilterBar } from '../FilterBar';
5
+ import { FileActivity } from '../FileActivity';
6
+ import type { FilterOptions } from '../../lib/types';
7
+
8
+ interface Props {
9
+ files: FileEntry[];
10
+ filterOptions: FilterOptions;
11
+ }
12
+
13
+ export function FilesView({ files, filterOptions }: Props) {
14
+ const [selectedPath, setSelectedPath] = useState<string | null>(null);
15
+
16
+ return (
17
+ <div className="flex flex-col h-full overflow-hidden">
18
+ <ViewHeader title="Files" icon="Files" />
19
+ <FilterBar options={filterOptions} resultCount={files.length} />
20
+ <FileActivity files={files} selectedPath={selectedPath} onSelect={setSelectedPath} />
21
+ </div>
22
+ );
23
+ }
@@ -0,0 +1,20 @@
1
+ import type { Graph3D } from '../../../../src/lib/graph';
2
+ import { ViewHeader } from '../ViewHeader';
3
+ import { FilterBar } from '../FilterBar';
4
+ import { Network3D } from '../Network3D';
5
+ import type { FilterOptions } from '../../lib/types';
6
+
7
+ interface Props {
8
+ graph: Graph3D;
9
+ filterOptions: FilterOptions;
10
+ }
11
+
12
+ export function NetworkView({ graph, filterOptions }: Props) {
13
+ return (
14
+ <div className="flex flex-col h-full overflow-hidden">
15
+ <ViewHeader title="Network" icon="Graph" />
16
+ <FilterBar options={filterOptions} resultCount={graph.nodes.length} />
17
+ <Network3D graph={graph} />
18
+ </div>
19
+ );
20
+ }