@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,135 @@
1
+ import { useMemo } from 'react';
2
+ import type { ClientSession } from '../../../../src/types';
3
+ import { ActiveSkillPanel } from '../ActiveSkillPanel';
4
+ import { TelemetryPanel } from '../TelemetryPanel';
5
+ import { ActivityGraph } from '../ActivityGraph';
6
+ import { ViewHeader } from '../ViewHeader';
7
+ import { usePinnedSessions } from '../../contexts/PinnedSessionsContext';
8
+ import { sourceIcon } from '../../../../src/lib/constants';
9
+ import { Icon } from '../ui/Icon';
10
+ import { truncate } from '../../../../src/lib/format';
11
+
12
+ interface Props {
13
+ sessions: Map<string, ClientSession>;
14
+ selectedSession: ClientSession | undefined;
15
+ onSelectSession: (id: string) => void;
16
+ }
17
+
18
+ export function Overview({ sessions, selectedSession, onSelectSession }: Props) {
19
+ const { pins } = usePinnedSessions();
20
+ const allSessions = useMemo(() => Array.from(sessions.values()), [sessions]);
21
+ const activeCount = useMemo(
22
+ () => allSessions.filter((s) => s.lifecycle === 'running').length,
23
+ [allSessions]
24
+ );
25
+
26
+ const topSkills = useMemo(() => {
27
+ const counts = new Map<string, number>();
28
+ for (const s of allSessions) {
29
+ const name = s.activeSkill?.name || s.skill;
30
+ if (name) counts.set(name, (counts.get(name) || 0) + 1);
31
+ }
32
+ return Array.from(counts.entries())
33
+ .sort((a, b) => b[1] - a[1])
34
+ .slice(0, 5);
35
+ }, [allSessions]);
36
+
37
+ const topTools = useMemo(() => {
38
+ const counts = new Map<string, number>();
39
+ for (const s of allSessions) {
40
+ for (const e of s.events) {
41
+ if (e.tool) counts.set(e.tool, (counts.get(e.tool) || 0) + 1);
42
+ }
43
+ }
44
+ return Array.from(counts.entries())
45
+ .sort((a, b) => b[1] - a[1])
46
+ .slice(0, 5);
47
+ }, [allSessions]);
48
+
49
+ return (
50
+ <div className="flex flex-col h-full overflow-hidden">
51
+ <ViewHeader title="Overview" icon="House" />
52
+ <div className="flex-1 overflow-y-auto p-5">
53
+ <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4">
54
+ <ActiveSkillPanel session={selectedSession} />
55
+ <TelemetryPanel session={selectedSession} />
56
+ <ActivityGraph session={selectedSession} />
57
+
58
+ <section className="panel p-5">
59
+ <h2 className="text-xs font-medium text-text-muted uppercase tracking-widest pb-3 border-b border-border-default">
60
+ Sessions
61
+ </h2>
62
+ <div className="pt-4 grid grid-cols-2 gap-3">
63
+ <div className="flex flex-col gap-1">
64
+ <span className="font-display text-4xl text-accent">{allSessions.length}</span>
65
+ <span className="text-[11px] uppercase tracking-widest text-text-muted">Total</span>
66
+ </div>
67
+ <div className="flex flex-col gap-1">
68
+ <span className="font-display text-4xl text-running">{activeCount}</span>
69
+ <span className="text-[11px] uppercase tracking-widest text-text-muted">Active</span>
70
+ </div>
71
+ <div className="flex flex-col gap-1">
72
+ <span className="font-display text-4xl text-accent">{pins.length}</span>
73
+ <span className="text-[11px] uppercase tracking-widest text-text-muted">Pinned</span>
74
+ </div>
75
+ </div>
76
+ </section>
77
+
78
+ <section className="panel p-5">
79
+ <h2 className="text-xs font-medium text-text-muted uppercase tracking-widest pb-3 border-b border-border-default">
80
+ Top Skills
81
+ </h2>
82
+ <div className="pt-3 flex flex-col gap-2">
83
+ {topSkills.length === 0 && <p className="text-sm text-text-muted">No skills observed.</p>}
84
+ {topSkills.map(([name, count]) => (
85
+ <div key={name} className="flex items-center justify-between text-sm">
86
+ <span className="text-text-primary">{name}</span>
87
+ <span className="text-text-muted tabular">{count}</span>
88
+ </div>
89
+ ))}
90
+ </div>
91
+ </section>
92
+
93
+ <section className="panel p-5">
94
+ <h2 className="text-xs font-medium text-text-muted uppercase tracking-widest pb-3 border-b border-border-default">
95
+ Top Tools
96
+ </h2>
97
+ <div className="pt-3 flex flex-col gap-2">
98
+ {topTools.length === 0 && <p className="text-sm text-text-muted">No tools observed.</p>}
99
+ {topTools.map(([name, count]) => (
100
+ <div key={name} className="flex items-center justify-between text-sm">
101
+ <span className="font-mono text-text-primary">{name}</span>
102
+ <span className="text-text-muted tabular">{count}</span>
103
+ </div>
104
+ ))}
105
+ </div>
106
+ </section>
107
+
108
+ <section className="panel p-5 md:col-span-2 xl:col-span-3">
109
+ <h2 className="text-xs font-medium text-text-muted uppercase tracking-widest pb-3 border-b border-border-default">
110
+ Recent Sessions
111
+ </h2>
112
+ <div className="pt-3 grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
113
+ {allSessions.slice(0, 6).map((s) => (
114
+ <button
115
+ key={s.id}
116
+ onClick={() => onSelectSession(s.id)}
117
+ className="flex items-center gap-3 p-3 rounded-lg border border-border-default bg-elevated hover:border-accent transition-colors text-left"
118
+ >
119
+ <Icon name={sourceIcon(s.source)} className="w-5 h-5 text-text-secondary" />
120
+ <div className="flex flex-col min-w-0">
121
+ <span className="text-sm text-text-primary font-mono truncate">
122
+ {truncate(s.activeSkill?.name || 'No active skill', 24)}
123
+ </span>
124
+ <span className="text-[11px] text-text-muted uppercase">{s.source}</span>
125
+ </div>
126
+ </button>
127
+ ))}
128
+ {allSessions.length === 0 && <p className="text-sm text-text-muted">No sessions yet.</p>}
129
+ </div>
130
+ </section>
131
+ </div>
132
+ </div>
133
+ </div>
134
+ );
135
+ }
@@ -0,0 +1,84 @@
1
+ import type { ClientSession } from '../../../../src/types';
2
+ import { usePinnedSessions } from '../../contexts/PinnedSessionsContext';
3
+ import { ViewHeader } from '../ViewHeader';
4
+ import { FilterBar } from '../FilterBar';
5
+ import { Icon } from '../ui/Icon';
6
+ import { formatDuration, formatTime, truncate } from '../../../../src/lib/format';
7
+ import type { FilterOptions } from '../../lib/types';
8
+
9
+ interface Props {
10
+ sessions: ClientSession[];
11
+ selectedSessionId: string | null;
12
+ filterOptions: FilterOptions;
13
+ onSelectSession: (id: string) => void;
14
+ }
15
+
16
+ function lifecycleColor(lifecycle: string): string {
17
+ switch (lifecycle) {
18
+ case 'starting':
19
+ return 'bg-warning';
20
+ case 'running':
21
+ return 'bg-running';
22
+ case 'ended':
23
+ return 'bg-text-muted';
24
+ default:
25
+ return 'bg-text-muted';
26
+ }
27
+ }
28
+
29
+ export function SessionsView({ sessions, selectedSessionId, filterOptions, onSelectSession }: Props) {
30
+ const { togglePin, isPinned } = usePinnedSessions();
31
+
32
+ return (
33
+ <div className="flex flex-col h-full overflow-hidden">
34
+ <ViewHeader title="Sessions" icon="Rows" />
35
+ <FilterBar options={filterOptions} resultCount={sessions.length} />
36
+ <div className="flex-1 overflow-y-auto p-5">
37
+ <div className="flex flex-col gap-2">
38
+ {sessions.map((s) => {
39
+ const duration = s.endedAt
40
+ ? formatDuration(s.endedAt - s.startTime)
41
+ : formatDuration(Date.now() - s.startTime);
42
+ const active = s.id === selectedSessionId;
43
+ const pinned = isPinned(s.id);
44
+ return (
45
+ <button
46
+ key={s.id}
47
+ onClick={() => onSelectSession(s.id)}
48
+ className={`group flex items-center gap-3 w-full text-left p-3 rounded-lg border transition-colors ${
49
+ active
50
+ ? 'bg-elevated border-accent'
51
+ : 'bg-surface border-border-default hover:bg-elevated'
52
+ }`}
53
+ >
54
+ <button
55
+ onClick={(e) => {
56
+ e.stopPropagation();
57
+ togglePin(s.id);
58
+ }}
59
+ aria-label={pinned ? 'Unpin session' : 'Pin session'}
60
+ className={`p-1 rounded hover:bg-base transition-colors ${
61
+ pinned ? 'text-accent' : 'text-text-muted group-hover:text-text-secondary'
62
+ }`}
63
+ >
64
+ <Icon name={pinned ? 'PushPinSlash' : 'PushPin'} className="w-4 h-4" />
65
+ </button>
66
+ <span className={`w-2.5 h-2.5 rounded-full ${lifecycleColor(s.lifecycle)}`} />
67
+ <div className="flex flex-col min-w-0 flex-1">
68
+ <span className="text-sm text-text-primary font-mono truncate">
69
+ {truncate(s.activeSkill?.name || s.id, 40)}
70
+ </span>
71
+ <span className="text-[11px] text-text-muted">
72
+ {formatTime(s.startTime)} · {duration} · {s.events.length} events
73
+ </span>
74
+ </div>
75
+ <span className="text-[11px] uppercase text-text-muted">{s.source}</span>
76
+ </button>
77
+ );
78
+ })}
79
+ {sessions.length === 0 && <p className="text-sm text-text-muted text-center py-8">No sessions match the filters.</p>}
80
+ </div>
81
+ </div>
82
+ </div>
83
+ );
84
+ }
@@ -0,0 +1,138 @@
1
+ import { useSettings } from '../../contexts/SettingsContext';
2
+ import { ViewHeader } from '../ViewHeader';
3
+ import { Icon } from '../ui/Icon';
4
+
5
+ export function SettingsView() {
6
+ const { settings, setSettings, reducedMotion } = useSettings();
7
+
8
+ return (
9
+ <div className="flex flex-col h-full overflow-hidden">
10
+ <ViewHeader title="Settings" icon="Gear" />
11
+ <div className="flex-1 overflow-y-auto p-5">
12
+ <div className="max-w-2xl flex flex-col gap-4">
13
+ <section className="panel p-5">
14
+ <h2 className="text-xs font-medium text-text-muted uppercase tracking-widest pb-3 border-b border-border-default">
15
+ Appearance
16
+ </h2>
17
+ <div className="pt-4 flex flex-col gap-4">
18
+ <div className="flex items-center justify-between">
19
+ <div>
20
+ <div className="text-sm text-text-primary">Theme</div>
21
+ <div className="text-xs text-text-muted">Choose your preferred color scheme.</div>
22
+ </div>
23
+ <select
24
+ value={settings.theme}
25
+ onChange={(e) => setSettings((s) => ({ ...s, theme: e.target.value as typeof s.theme }))}
26
+ className="h-9 px-3 rounded-lg bg-elevated border border-border-default text-sm text-text-primary outline-none focus:border-accent"
27
+ >
28
+ <option value="system">System</option>
29
+ <option value="dark">Dark</option>
30
+ <option value="light">Light</option>
31
+ </select>
32
+ </div>
33
+
34
+ <div className="flex items-center justify-between">
35
+ <div>
36
+ <div className="text-sm text-text-primary">Density</div>
37
+ <div className="text-xs text-text-muted">Control how compact lists appear.</div>
38
+ </div>
39
+ <div className="flex items-center gap-2">
40
+ <button
41
+ onClick={() => setSettings((s) => ({ ...s, density: 'comfortable' }))}
42
+ className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg border text-xs transition-colors ${
43
+ settings.density === 'comfortable'
44
+ ? 'border-accent text-accent bg-accent/10'
45
+ : 'border-border-default text-text-secondary hover:bg-elevated'
46
+ }`}
47
+ >
48
+ <Icon name="ArrowsOutLineVertical" className="w-4 h-4" />
49
+ Comfortable
50
+ </button>
51
+ <button
52
+ onClick={() => setSettings((s) => ({ ...s, density: 'compact' }))}
53
+ className={`flex items-center gap-1.5 px-3 py-1.5 rounded-lg border text-xs transition-colors ${
54
+ settings.density === 'compact'
55
+ ? 'border-accent text-accent bg-accent/10'
56
+ : 'border-border-default text-text-secondary hover:bg-elevated'
57
+ }`}
58
+ >
59
+ <Icon name="ArrowsInLineVertical" className="w-4 h-4" />
60
+ Compact
61
+ </button>
62
+ </div>
63
+ </div>
64
+
65
+ <div className="flex items-center justify-between">
66
+ <div>
67
+ <div className="text-sm text-text-primary">Reduced motion</div>
68
+ <div className="text-xs text-text-muted">Disable animations throughout the dashboard.</div>
69
+ </div>
70
+ <button
71
+ onClick={() => setSettings((s) => ({ ...s, reducedMotion: !s.reducedMotion }))}
72
+ className={`relative w-11 h-6 rounded-full transition-colors ${
73
+ settings.reducedMotion ? 'bg-accent' : 'bg-border-strong'
74
+ }`}
75
+ aria-pressed={settings.reducedMotion}
76
+ >
77
+ <span
78
+ className={`absolute top-1 left-1 w-4 h-4 rounded-full bg-white transition-transform ${
79
+ settings.reducedMotion ? 'translate-x-5' : 'translate-x-0'
80
+ }`}
81
+ />
82
+ </button>
83
+ </div>
84
+ {reducedMotion && (
85
+ <p className="text-xs text-text-muted">OS reduced-motion preference is also enabled.</p>
86
+ )}
87
+ </div>
88
+ </section>
89
+
90
+ <section className="panel p-5">
91
+ <h2 className="text-xs font-medium text-text-muted uppercase tracking-widest pb-3 border-b border-border-default">
92
+ Behavior
93
+ </h2>
94
+ <div className="pt-4 flex flex-col gap-4">
95
+ <div className="flex items-center justify-between">
96
+ <div>
97
+ <div className="text-sm text-text-primary">Auto-follow active session</div>
98
+ <div className="text-xs text-text-muted">Automatically select the running session.</div>
99
+ </div>
100
+ <button
101
+ onClick={() => setSettings((s) => ({ ...s, autoFollowActive: !s.autoFollowActive }))}
102
+ className={`relative w-11 h-6 rounded-full transition-colors ${
103
+ settings.autoFollowActive ? 'bg-accent' : 'bg-border-strong'
104
+ }`}
105
+ aria-pressed={settings.autoFollowActive}
106
+ >
107
+ <span
108
+ className={`absolute top-1 left-1 w-4 h-4 rounded-full bg-white transition-transform ${
109
+ settings.autoFollowActive ? 'translate-x-5' : 'translate-x-0'
110
+ }`}
111
+ />
112
+ </button>
113
+ </div>
114
+
115
+ <div className="flex items-center justify-between">
116
+ <div>
117
+ <div className="text-sm text-text-primary">Max events per session</div>
118
+ <div className="text-xs text-text-muted">Limit how many events are kept in memory.</div>
119
+ </div>
120
+ <input
121
+ type="number"
122
+ min={10}
123
+ max={1000}
124
+ value={settings.maxEvents}
125
+ onChange={(e) => {
126
+ const n = parseInt(e.target.value, 10);
127
+ if (!Number.isNaN(n)) setSettings((s) => ({ ...s, maxEvents: Math.max(10, Math.min(1000, n)) }));
128
+ }}
129
+ className="w-24 h-9 px-3 rounded-lg bg-elevated border border-border-default text-sm text-text-primary outline-none focus:border-accent"
130
+ />
131
+ </div>
132
+ </div>
133
+ </section>
134
+ </div>
135
+ </div>
136
+ </div>
137
+ );
138
+ }
@@ -0,0 +1,92 @@
1
+ import { useMemo } from 'react';
2
+ import type { ToolInvocation } from '../../../../src/lib/invocations';
3
+ import { operationType } from '../../../../src/lib/invocations';
4
+ import { resolvePath } from '../../../../src/lib/paths';
5
+ import { ViewHeader } from '../ViewHeader';
6
+ import { FilterBar } from '../FilterBar';
7
+ import type { FilterOptions } from '../../lib/types';
8
+ import { Icon } from '../ui/Icon';
9
+
10
+ interface Props {
11
+ invocations: ToolInvocation[];
12
+ filterOptions: FilterOptions;
13
+ }
14
+
15
+ export function SkillsView({ invocations, filterOptions }: Props) {
16
+ const stats = useMemo(() => {
17
+ const skills = new Map<string, number>();
18
+ const tools = new Map<string, number>();
19
+ const files = new Set<string>();
20
+ for (const inv of invocations) {
21
+ if (inv.skill) skills.set(inv.skill, (skills.get(inv.skill) || 0) + 1);
22
+ tools.set(inv.tool, (tools.get(inv.tool) || 0) + 1);
23
+ const path = resolvePath(inv.input, inv.output);
24
+ if (path) files.add(path);
25
+ }
26
+ return {
27
+ skills: Array.from(skills.entries()).sort((a, b) => b[1] - a[1]),
28
+ tools: Array.from(tools.entries()).sort((a, b) => b[1] - a[1]),
29
+ reads: invocations.filter((i) => operationType(i.tool) === 'read').length,
30
+ edits: invocations.filter((i) => operationType(i.tool) === 'edit').length,
31
+ fileCount: files.size,
32
+ };
33
+ }, [invocations]);
34
+
35
+ return (
36
+ <div className="flex flex-col h-full overflow-hidden">
37
+ <ViewHeader title="Skills" icon="ChartPie" />
38
+ <FilterBar options={filterOptions} resultCount={invocations.length} />
39
+ <div className="flex-1 overflow-y-auto p-5">
40
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-4">
41
+ <div className="panel p-4 flex flex-col gap-1">
42
+ <span className="font-display text-4xl text-accent">{stats.skills.length}</span>
43
+ <span className="text-[11px] uppercase tracking-widest text-text-muted">Skills</span>
44
+ </div>
45
+ <div className="panel p-4 flex flex-col gap-1">
46
+ <span className="font-display text-4xl text-accent">{stats.tools.length}</span>
47
+ <span className="text-[11px] uppercase tracking-widest text-text-muted">Tools</span>
48
+ </div>
49
+ <div className="panel p-4 flex flex-col gap-1">
50
+ <span className="font-display text-4xl text-accent">{stats.fileCount}</span>
51
+ <span className="text-[11px] uppercase tracking-widest text-text-muted">Files touched</span>
52
+ </div>
53
+ </div>
54
+
55
+ <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
56
+ <section className="panel p-5">
57
+ <h2 className="text-xs font-medium text-text-muted uppercase tracking-widest pb-3 border-b border-border-default">
58
+ Skill usage
59
+ </h2>
60
+ <div className="pt-3 flex flex-col gap-2">
61
+ {stats.skills.length === 0 && <p className="text-sm text-text-muted">No skills observed.</p>}
62
+ {stats.skills.map(([name, count]) => (
63
+ <div key={name} className="flex items-center justify-between text-sm">
64
+ <div className="flex items-center gap-2">
65
+ <Icon name="Circle" className="w-2 h-2 text-accent" />
66
+ <span className="text-text-primary">{name}</span>
67
+ </div>
68
+ <span className="text-text-muted tabular">{count}</span>
69
+ </div>
70
+ ))}
71
+ </div>
72
+ </section>
73
+
74
+ <section className="panel p-5">
75
+ <h2 className="text-xs font-medium text-text-muted uppercase tracking-widest pb-3 border-b border-border-default">
76
+ Tool usage
77
+ </h2>
78
+ <div className="pt-3 flex flex-col gap-2">
79
+ {stats.tools.length === 0 && <p className="text-sm text-text-muted">No tools observed.</p>}
80
+ {stats.tools.map(([name, count]) => (
81
+ <div key={name} className="flex items-center justify-between text-sm">
82
+ <span className="font-mono text-text-primary">{name}</span>
83
+ <span className="text-text-muted tabular">{count}</span>
84
+ </div>
85
+ ))}
86
+ </div>
87
+ </section>
88
+ </div>
89
+ </div>
90
+ </div>
91
+ );
92
+ }
@@ -0,0 +1,46 @@
1
+ import { useState } from 'react';
2
+ import type { ToolInvocation } from '../../../../src/lib/invocations';
3
+ import { ViewHeader } from '../ViewHeader';
4
+ import { FilterBar } from '../FilterBar';
5
+ import { Timeline } from '../Timeline';
6
+ import { toJson, download, filename, toExportableEvent } from '../../lib/export';
7
+ import type { FilterOptions } from '../../lib/types';
8
+
9
+ interface Props {
10
+ invocations: ToolInvocation[];
11
+ filterOptions: FilterOptions;
12
+ onMouseEnter?: () => void;
13
+ onMouseLeave?: () => void;
14
+ }
15
+
16
+ export function TimelineView({ invocations, filterOptions, onMouseEnter, onMouseLeave }: Props) {
17
+ const [expandedIds, setExpandedIds] = useState<Set<string>>(new Set());
18
+
19
+ function toggleExpanded(id: string) {
20
+ setExpandedIds((prev) => {
21
+ const next = new Set(prev);
22
+ if (next.has(id)) next.delete(id);
23
+ else next.add(id);
24
+ return next;
25
+ });
26
+ }
27
+
28
+ function handleExport() {
29
+ const events = invocations.map(toExportableEvent);
30
+ download(toJson(events), filename('json'));
31
+ }
32
+
33
+ return (
34
+ <div className="flex flex-col h-full overflow-hidden">
35
+ <ViewHeader title="Timeline" icon="Clock" />
36
+ <FilterBar options={filterOptions} resultCount={invocations.length} onExport={handleExport} />
37
+ <Timeline
38
+ invocations={invocations}
39
+ expandedIds={expandedIds}
40
+ onToggle={toggleExpanded}
41
+ onMouseEnter={onMouseEnter}
42
+ onMouseLeave={onMouseLeave}
43
+ />
44
+ </div>
45
+ );
46
+ }
@@ -0,0 +1,41 @@
1
+ import { createContext, useCallback, useContext, useState, type ReactNode } from 'react';
2
+ import type { FilterState } from '../lib/types';
3
+ import { DEFAULT_FILTER_STATE } from '../lib/types';
4
+
5
+ interface FilterContextValue {
6
+ filters: FilterState;
7
+ setFilters: (updater: Partial<FilterState> | ((prev: FilterState) => FilterState)) => void;
8
+ resetFilters: () => void;
9
+ }
10
+
11
+ const FilterContext = createContext<FilterContextValue | null>(null);
12
+
13
+ export function FilterProvider({ children }: { children: ReactNode }) {
14
+ const [filters, setFiltersState] = useState<FilterState>(DEFAULT_FILTER_STATE);
15
+
16
+ const setFilters = useCallback(
17
+ (updater: Partial<FilterState> | ((prev: FilterState) => FilterState)) => {
18
+ setFiltersState((prev) => {
19
+ if (typeof updater === 'function') {
20
+ return (updater as (prev: FilterState) => FilterState)(prev);
21
+ }
22
+ return { ...prev, ...updater };
23
+ });
24
+ },
25
+ []
26
+ );
27
+
28
+ const resetFilters = useCallback(() => setFiltersState(DEFAULT_FILTER_STATE), []);
29
+
30
+ return (
31
+ <FilterContext.Provider value={{ filters, setFilters, resetFilters }}>
32
+ {children}
33
+ </FilterContext.Provider>
34
+ );
35
+ }
36
+
37
+ export function useFilters(): FilterContextValue {
38
+ const ctx = useContext(FilterContext);
39
+ if (!ctx) throw new Error('useFilters must be used within FilterProvider');
40
+ return ctx;
41
+ }
@@ -0,0 +1,80 @@
1
+ import { createContext, useCallback, useContext, useState, type ReactNode } from 'react';
2
+ import type { PinnedSession } from '../lib/types';
3
+
4
+ interface PinnedSessionsContextValue {
5
+ pins: PinnedSession[];
6
+ addPin: (id: string) => void;
7
+ removePin: (id: string) => void;
8
+ togglePin: (id: string) => void;
9
+ isPinned: (id: string) => boolean;
10
+ }
11
+
12
+ const STORAGE_KEY = 'crewloop-dashboard-pins';
13
+
14
+ const PinnedSessionsContext = createContext<PinnedSessionsContextValue | null>(null);
15
+
16
+ function loadPins(): PinnedSession[] {
17
+ try {
18
+ const raw = localStorage.getItem(STORAGE_KEY);
19
+ if (!raw) return [];
20
+ const parsed = JSON.parse(raw);
21
+ if (Array.isArray(parsed)) return parsed.filter((p): p is PinnedSession => p && typeof p.id === 'string' && typeof p.pinnedAt === 'number');
22
+ } catch {
23
+ // ignore
24
+ }
25
+ return [];
26
+ }
27
+
28
+ function savePins(pins: PinnedSession[]): void {
29
+ try {
30
+ localStorage.setItem(STORAGE_KEY, JSON.stringify(pins));
31
+ } catch {
32
+ // ignore
33
+ }
34
+ }
35
+
36
+ export function PinnedSessionsProvider({ children }: { children: ReactNode }) {
37
+ const [pins, setPins] = useState<PinnedSession[]>(() => loadPins());
38
+
39
+ const addPin = useCallback((id: string) => {
40
+ setPins((prev) => {
41
+ if (prev.some((p) => p.id === id)) return prev;
42
+ const next = [...prev, { id, pinnedAt: Date.now() }];
43
+ savePins(next);
44
+ return next;
45
+ });
46
+ }, []);
47
+
48
+ const removePin = useCallback((id: string) => {
49
+ setPins((prev) => {
50
+ const next = prev.filter((p) => p.id !== id);
51
+ savePins(next);
52
+ return next;
53
+ });
54
+ }, []);
55
+
56
+ const togglePin = useCallback(
57
+ (id: string) => {
58
+ if (pins.some((p) => p.id === id)) removePin(id);
59
+ else addPin(id);
60
+ },
61
+ [pins, addPin, removePin]
62
+ );
63
+
64
+ const isPinned = useCallback(
65
+ (id: string) => pins.some((p) => p.id === id),
66
+ [pins]
67
+ );
68
+
69
+ return (
70
+ <PinnedSessionsContext.Provider value={{ pins, addPin, removePin, togglePin, isPinned }}>
71
+ {children}
72
+ </PinnedSessionsContext.Provider>
73
+ );
74
+ }
75
+
76
+ export function usePinnedSessions(): PinnedSessionsContextValue {
77
+ const ctx = useContext(PinnedSessionsContext);
78
+ if (!ctx) throw new Error('usePinnedSessions must be used within PinnedSessionsProvider');
79
+ return ctx;
80
+ }