@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,20 @@
1
+ import type { FileEntry } from '../../../src/lib/invocations';
2
+ import { FileList } from './FileList';
3
+ import { FileDiff } from './FileDiff';
4
+
5
+ interface Props {
6
+ files: FileEntry[];
7
+ selectedPath: string | null;
8
+ onSelect: (path: string) => void;
9
+ }
10
+
11
+ export function FileActivity({ files, selectedPath, onSelect }: Props) {
12
+ const selectedFile = files.find((f) => f.path === selectedPath);
13
+
14
+ return (
15
+ <div className="flex-1 flex min-h-0 overflow-hidden">
16
+ <FileList files={files} selectedPath={selectedPath} onSelect={onSelect} />
17
+ <FileDiff file={selectedFile} />
18
+ </div>
19
+ );
20
+ }
@@ -0,0 +1,68 @@
1
+ import { escapeHtml } from '../../../src/lib/format';
2
+ import type { FileEntry } from '../../../src/lib/invocations';
3
+ import { StatusBadge } from './ui/StatusBadge';
4
+
5
+ interface Props {
6
+ file: FileEntry | undefined;
7
+ }
8
+
9
+ export function FileDiff({ file }: Props) {
10
+ if (!file) {
11
+ return (
12
+ <div className="flex-1 flex items-center justify-center text-text-muted text-sm">
13
+ Select a file to view activity.
14
+ </div>
15
+ );
16
+ }
17
+
18
+ const latest = file.ops[file.ops.length - 1];
19
+ const lines = file.snippet ? String(file.snippet).split('\n').slice(0, 80) : [];
20
+
21
+ return (
22
+ <div className="flex-1 min-w-0 flex flex-col overflow-hidden">
23
+ <header className="flex flex-col gap-2 p-4 pb-3 border-b border-border-default">
24
+ <span className="text-text-primary break-all text-sm font-mono">{file.path}</span>
25
+ <div className="flex gap-2 flex-wrap">
26
+ {file.ops.map((op) => (
27
+ <span
28
+ key={op.id}
29
+ className={`text-[10px] font-semibold uppercase px-1.5 py-0.5 rounded border ${
30
+ op.type === 'read'
31
+ ? 'text-running border-running/35'
32
+ : op.type === 'edit'
33
+ ? 'text-success border-success/35'
34
+ : 'text-text-muted border-border-default'
35
+ }`}
36
+ >
37
+ {op.type}
38
+ </span>
39
+ ))}
40
+ <StatusBadge status={latest.status} />
41
+ </div>
42
+ </header>
43
+ <div className="flex-1 overflow-auto p-4 font-mono text-[13px]">
44
+ {lines.length === 0 ? (
45
+ <p className="text-text-muted">No diff or content snippet available.</p>
46
+ ) : (
47
+ <div className="min-w-full inline-block">
48
+ {lines.map((line, i) => {
49
+ let cls = 'text-text-secondary px-1 whitespace-pre';
50
+ if (line.startsWith('+')) cls = 'text-success bg-success/5 px-1 whitespace-pre';
51
+ else if (line.startsWith('-')) cls = 'text-error bg-error/5 px-1 whitespace-pre';
52
+ else if (line.startsWith('@@') || line.startsWith('---') || line.startsWith('+++'))
53
+ cls = 'text-text-muted px-1 whitespace-pre';
54
+ return (
55
+ <div key={i} className={cls}>
56
+ <span dangerouslySetInnerHTML={{ __html: escapeHtml(line) }} />
57
+ </div>
58
+ );
59
+ })}
60
+ {String(file.snippet).split('\n').length > 80 && (
61
+ <p className="text-text-muted mt-2 text-xs">Content truncated.</p>
62
+ )}
63
+ </div>
64
+ )}
65
+ </div>
66
+ </div>
67
+ );
68
+ }
@@ -0,0 +1,64 @@
1
+ import type { FileEntry } from '../../../src/lib/invocations';
2
+ import { truncate } from '../../../src/lib/format';
3
+ import { Icon } from './ui/Icon';
4
+ import { StatusBadge } from './ui/StatusBadge';
5
+
6
+ interface Props {
7
+ files: FileEntry[];
8
+ selectedPath: string | null;
9
+ onSelect: (path: string) => void;
10
+ }
11
+
12
+ export function FileList({ files, selectedPath, onSelect }: Props) {
13
+ if (files.length === 0) {
14
+ return (
15
+ <div className="w-full md:w-72 flex-shrink-0 flex items-center justify-center text-text-muted text-sm border-r border-border-default">
16
+ No file activity matches the filters.
17
+ </div>
18
+ );
19
+ }
20
+
21
+ return (
22
+ <div className="w-full md:w-72 flex-shrink-0 overflow-y-auto border-r border-border-default p-2">
23
+ {files.map((file) => {
24
+ const latest = file.ops[file.ops.length - 1];
25
+ const isActive = file.path === selectedPath;
26
+ return (
27
+ <button
28
+ key={file.path}
29
+ onClick={() => onSelect(file.path)}
30
+ className={`file-list-item w-full flex flex-col gap-1.5 text-left p-2.5 rounded border transition-colors ${
31
+ isActive
32
+ ? 'bg-elevated border-accent'
33
+ : 'border-transparent hover:bg-elevated'
34
+ }`}
35
+ >
36
+ <div className="flex items-center gap-2 min-w-0 text-text-primary font-mono text-[13px]">
37
+ <Icon name="FileText" className="w-4 h-4 text-accent flex-shrink-0" />
38
+ <span className="truncate" title={file.path}>
39
+ {truncate(file.path, 32)}
40
+ </span>
41
+ </div>
42
+ <div className="flex gap-1.5 flex-wrap items-center">
43
+ {file.ops.map((op) => (
44
+ <span
45
+ key={op.id}
46
+ className={`text-[10px] font-semibold uppercase px-1.5 py-0.5 rounded border ${
47
+ op.type === 'read'
48
+ ? 'text-running border-running/35'
49
+ : op.type === 'edit'
50
+ ? 'text-success border-success/35'
51
+ : 'text-text-muted border-border-default'
52
+ }`}
53
+ >
54
+ {op.type}
55
+ </span>
56
+ ))}
57
+ <StatusBadge status={latest.status} />
58
+ </div>
59
+ </button>
60
+ );
61
+ })}
62
+ </div>
63
+ );
64
+ }
@@ -0,0 +1,208 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import type { FilterOptions, TimeRange } from '../lib/types';
3
+ import { useFilters } from '../contexts/FilterContext';
4
+ import { Icon } from './ui/Icon';
5
+
6
+ interface Props {
7
+ options: FilterOptions;
8
+ resultCount: number;
9
+ onExport?: () => void;
10
+ }
11
+
12
+ function FilterPopover({
13
+ label,
14
+ activeCount,
15
+ children,
16
+ }: {
17
+ label: string;
18
+ activeCount: number;
19
+ children: React.ReactNode;
20
+ }) {
21
+ const [open, setOpen] = useState(false);
22
+ const ref = useRef<HTMLDivElement>(null);
23
+
24
+ useEffect(() => {
25
+ if (!open) return;
26
+ function onMouseDown(e: MouseEvent) {
27
+ if (!ref.current?.contains(e.target as Node)) {
28
+ setOpen(false);
29
+ }
30
+ }
31
+ document.addEventListener('mousedown', onMouseDown);
32
+ return () => document.removeEventListener('mousedown', onMouseDown);
33
+ }, [open]);
34
+
35
+ return (
36
+ <div ref={ref} className="relative">
37
+ <button
38
+ onClick={() => setOpen((v) => !v)}
39
+ className={`flex items-center gap-1.5 px-2.5 py-1.5 rounded-lg border text-xs transition-colors ${
40
+ activeCount > 0
41
+ ? 'border-accent text-accent bg-accent/10'
42
+ : 'border-border-default text-text-secondary hover:border-strong'
43
+ }`}
44
+ >
45
+ <span>{label}</span>
46
+ {activeCount > 0 && (
47
+ <span className="px-1 rounded bg-accent text-white text-[10px]">{activeCount}</span>
48
+ )}
49
+ </button>
50
+ {open && (
51
+ <div className="absolute top-[calc(100%+6px)] left-0 z-30 min-w-[180px] max-h-64 overflow-y-auto bg-surface border border-border-default rounded-lg shadow-lg p-2">
52
+ {children}
53
+ </div>
54
+ )}
55
+ </div>
56
+ );
57
+ }
58
+
59
+ function CheckboxRow({
60
+ label,
61
+ checked,
62
+ onChange,
63
+ }: {
64
+ label: string;
65
+ checked: boolean;
66
+ onChange: (checked: boolean) => void;
67
+ }) {
68
+ return (
69
+ <label className="flex items-center gap-2 px-2 py-1.5 rounded hover:bg-elevated cursor-pointer text-sm text-text-secondary">
70
+ <input
71
+ type="checkbox"
72
+ checked={checked}
73
+ onChange={(e) => onChange(e.target.checked)}
74
+ className="accent-accent"
75
+ />
76
+ <span className="truncate">{label}</span>
77
+ </label>
78
+ );
79
+ }
80
+
81
+ export function FilterBar({ options, resultCount, onExport }: Props) {
82
+ const { filters, setFilters, resetFilters } = useFilters();
83
+ const activeCount =
84
+ filters.sources.length +
85
+ filters.skills.length +
86
+ filters.statuses.length +
87
+ filters.tools.length +
88
+ filters.opTypes.length +
89
+ (filters.timeRange !== 'all' ? 1 : 0);
90
+
91
+ function toggleList<T extends string>(current: T[], value: T): T[] {
92
+ return current.includes(value) ? current.filter((v) => v !== value) : [...current, value];
93
+ }
94
+
95
+ return (
96
+ <div className="filter-bar flex flex-col gap-2 px-5 py-3 border-b border-border-default flex-shrink-0">
97
+ <div className="flex flex-wrap items-center gap-2">
98
+ <div className="relative flex items-center flex-1 min-w-[200px]">
99
+ <Icon name="MagnifyingGlass" className="absolute left-2.5 w-4 h-4 text-text-muted" />
100
+ <input
101
+ type="text"
102
+ value={filters.query}
103
+ onChange={(e) => setFilters({ query: e.target.value })}
104
+ placeholder="Filter events..."
105
+ className="w-full h-9 pl-9 pr-8 rounded-lg bg-elevated border border-border-default text-sm text-text-primary placeholder:text-text-muted outline-none focus:border-accent"
106
+ />
107
+ {filters.query && (
108
+ <button
109
+ onClick={() => setFilters({ query: '' })}
110
+ aria-label="Clear search"
111
+ className="absolute right-2 text-text-muted hover:text-text-primary"
112
+ >
113
+ <Icon name="XCircle" className="w-4 h-4" />
114
+ </button>
115
+ )}
116
+ </div>
117
+
118
+ <FilterPopover label="Source" activeCount={filters.sources.length}>
119
+ {options.sources.map((s) => (
120
+ <CheckboxRow
121
+ key={s}
122
+ label={s}
123
+ checked={filters.sources.includes(s)}
124
+ onChange={() => setFilters({ sources: toggleList(filters.sources, s) })}
125
+ />
126
+ ))}
127
+ </FilterPopover>
128
+
129
+ <FilterPopover label="Skill" activeCount={filters.skills.length}>
130
+ {options.skills.map((s) => (
131
+ <CheckboxRow
132
+ key={s}
133
+ label={s}
134
+ checked={filters.skills.includes(s)}
135
+ onChange={() => setFilters({ skills: toggleList(filters.skills, s) })}
136
+ />
137
+ ))}
138
+ </FilterPopover>
139
+
140
+ <FilterPopover label="Status" activeCount={filters.statuses.length}>
141
+ {options.statuses.map((s) => (
142
+ <CheckboxRow
143
+ key={s}
144
+ label={s}
145
+ checked={filters.statuses.includes(s)}
146
+ onChange={() => setFilters({ statuses: toggleList(filters.statuses, s) })}
147
+ />
148
+ ))}
149
+ </FilterPopover>
150
+
151
+ <FilterPopover label="Tool" activeCount={filters.tools.length}>
152
+ {options.tools.map((t) => (
153
+ <CheckboxRow
154
+ key={t}
155
+ label={t}
156
+ checked={filters.tools.includes(t)}
157
+ onChange={() => setFilters({ tools: toggleList(filters.tools, t) })}
158
+ />
159
+ ))}
160
+ </FilterPopover>
161
+
162
+ <FilterPopover label="Op" activeCount={filters.opTypes.length}>
163
+ {options.opTypes.map((o) => (
164
+ <CheckboxRow
165
+ key={o}
166
+ label={o}
167
+ checked={filters.opTypes.includes(o)}
168
+ onChange={() => setFilters({ opTypes: toggleList(filters.opTypes, o) })}
169
+ />
170
+ ))}
171
+ </FilterPopover>
172
+
173
+ <FilterPopover label="Time" activeCount={filters.timeRange !== 'all' ? 1 : 0}>
174
+ {(['all', '1m', '5m', '15m', '1h', '24h', 'session'] as TimeRange[]).map((r) => (
175
+ <CheckboxRow
176
+ key={r}
177
+ label={r === 'all' ? 'All time' : r === 'session' ? 'This session' : r}
178
+ checked={filters.timeRange === r}
179
+ onChange={() => setFilters({ timeRange: r })}
180
+ />
181
+ ))}
182
+ </FilterPopover>
183
+
184
+ {activeCount > 0 && (
185
+ <button
186
+ onClick={resetFilters}
187
+ className="text-xs text-text-muted hover:text-text-primary transition-colors"
188
+ >
189
+ Clear all
190
+ </button>
191
+ )}
192
+
193
+ {onExport && (
194
+ <button
195
+ onClick={onExport}
196
+ aria-label="Export JSON"
197
+ 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"
198
+ >
199
+ <Icon name="DownloadSimple" className="w-4 h-4" />
200
+ </button>
201
+ )}
202
+ </div>
203
+ <div className="text-xs text-text-muted">
204
+ {resultCount} result{resultCount === 1 ? '' : 's'}
205
+ </div>
206
+ </div>
207
+ );
208
+ }
@@ -0,0 +1,178 @@
1
+ import { useEffect, useMemo, useRef, useState } from 'react';
2
+ import ForceGraph3D from 'react-force-graph-3d';
3
+ import type { ForceGraphMethods } from 'react-force-graph-3d';
4
+ import type { Graph3D, GraphLink, GraphNode } from '../../../src/lib/graph';
5
+ import { TYPE_COLORS } from '../../../src/lib/constants';
6
+ import { useSettings } from '../contexts/SettingsContext';
7
+ import { Icon } from './ui/Icon';
8
+
9
+ interface Props {
10
+ graph: Graph3D;
11
+ }
12
+
13
+ export function Network3D({ graph }: Props) {
14
+ const { resolvedTheme: theme, reducedMotion } = useSettings();
15
+ const fgRef = useRef<ForceGraphMethods | undefined>(undefined);
16
+ const [selected, setSelected] = useState<GraphNode | null>(null);
17
+ const [hovered, setHovered] = useState<GraphNode | null>(null);
18
+ const [error, setError] = useState<string | null>(null);
19
+
20
+ const stableNodesRef = useRef<Map<string, GraphNode>>(new Map());
21
+ const stableLinksRef = useRef<Map<string, GraphLink>>(new Map());
22
+
23
+ const normalizedGraph = useMemo(() => {
24
+ const next = graph;
25
+ const nodeIds = new Set<string>();
26
+ const nodes: GraphNode[] = [];
27
+ for (const n of next.nodes) {
28
+ nodeIds.add(n.id);
29
+ const existing = stableNodesRef.current.get(n.id);
30
+ if (existing) {
31
+ existing.weight = n.weight;
32
+ existing.label = n.label;
33
+ existing.type = n.type;
34
+ nodes.push(existing);
35
+ } else {
36
+ stableNodesRef.current.set(n.id, n);
37
+ nodes.push(n);
38
+ }
39
+ }
40
+ for (const id of Array.from(stableNodesRef.current.keys())) {
41
+ if (!nodeIds.has(id)) stableNodesRef.current.delete(id);
42
+ }
43
+
44
+ const linkKeys = new Set<string>();
45
+ const links: GraphLink[] = [];
46
+ for (const l of next.links) {
47
+ const key = `${l.source}→${l.target}`;
48
+ linkKeys.add(key);
49
+ const existing = stableLinksRef.current.get(key);
50
+ if (existing) {
51
+ existing.weight = l.weight;
52
+ links.push(existing);
53
+ } else {
54
+ stableLinksRef.current.set(key, l);
55
+ links.push(l);
56
+ }
57
+ }
58
+ for (const key of Array.from(stableLinksRef.current.keys())) {
59
+ if (!linkKeys.has(key)) stableLinksRef.current.delete(key);
60
+ }
61
+ return { nodes, links };
62
+ }, [graph]);
63
+
64
+ const colors = useMemo(() => {
65
+ const root = getComputedStyle(document.documentElement);
66
+ const resolve = (token: string) => {
67
+ const raw = TYPE_COLORS[token];
68
+ if (!raw.startsWith('var(')) return raw;
69
+ const name = raw.slice(4, -1).trim();
70
+ return root.getPropertyValue(name).trim() || '#888888';
71
+ };
72
+ return {
73
+ skill: resolve('skill'),
74
+ tool: resolve('tool'),
75
+ file: resolve('file'),
76
+ link: '#3f3f46',
77
+ };
78
+ }, [theme]);
79
+
80
+ useEffect(() => {
81
+ try {
82
+ const canvas = document.createElement('canvas');
83
+ const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
84
+ if (!gl) setError('WebGL is not available in this browser.');
85
+ } catch {
86
+ setError('Could not initialize WebGL.');
87
+ }
88
+ }, []);
89
+
90
+ useEffect(() => {
91
+ if (!fgRef.current || normalizedGraph.nodes.length === 0) return;
92
+ const controls = (fgRef.current as unknown as { zoomToFit?: (duration: number) => void }).zoomToFit;
93
+ if (controls) controls(reducedMotion ? 0 : 400);
94
+ }, [normalizedGraph, reducedMotion]);
95
+
96
+ if (error) {
97
+ return (
98
+ <div className="flex-1 flex flex-col items-center justify-center gap-3 text-text-muted">
99
+ <Icon name="Monitor" className="w-10 h-10" />
100
+ <p>{error}</p>
101
+ </div>
102
+ );
103
+ }
104
+
105
+ if (normalizedGraph.nodes.length === 0) {
106
+ return (
107
+ <div className="flex-1 flex items-center justify-center text-text-muted">
108
+ Waiting for agent activity...
109
+ </div>
110
+ );
111
+ }
112
+
113
+ const linkSourceId = (l: GraphLink) => (typeof l.source === 'string' ? l.source : (l.source as GraphNode).id);
114
+ const linkTargetId = (l: GraphLink) => (typeof l.target === 'string' ? l.target : (l.target as GraphNode).id);
115
+
116
+ const connected = selected
117
+ ? normalizedGraph.links.filter((l) => linkSourceId(l) === selected.id || linkTargetId(l) === selected.id)
118
+ : [];
119
+
120
+ return (
121
+ <div className="flex-1 relative min-h-0">
122
+ <ForceGraph3D
123
+ ref={fgRef}
124
+ graphData={normalizedGraph}
125
+ nodeColor={(n) => colors[(n as GraphNode).type] || colors.file}
126
+ nodeRelSize={4}
127
+ nodeVal={(n) => Math.max(1, (n as GraphNode).weight || 1)}
128
+ nodeLabel="label"
129
+ linkWidth={1}
130
+ linkColor={() => colors.link}
131
+ backgroundColor="rgba(0,0,0,0)"
132
+ showNavInfo={false}
133
+ enableNavigationControls
134
+ onNodeHover={(node) => setHovered(node ? (node as GraphNode) : null)}
135
+ onNodeClick={(node) => {
136
+ const n = node as GraphNode;
137
+ setSelected(n);
138
+ if (fgRef.current && n.x != null && n.y != null && n.z != null) {
139
+ const coords = n as unknown as { x: number; y: number; z: number };
140
+ fgRef.current.cameraPosition(
141
+ { x: coords.x + 40, y: coords.y + 40, z: coords.z + 40 },
142
+ coords,
143
+ reducedMotion ? 0 : 400
144
+ );
145
+ }
146
+ }}
147
+ onEngineStop={() => {
148
+ const controls = (fgRef.current as unknown as { zoomToFit?: (duration: number) => void })?.zoomToFit;
149
+ if (controls) controls(reducedMotion ? 0 : 400);
150
+ }}
151
+ warmupTicks={reducedMotion ? 0 : 10}
152
+ cooldownTicks={reducedMotion ? 0 : 120}
153
+ />
154
+ {(selected || hovered) && (
155
+ <div className="absolute top-3 right-3 w-64 p-3 bg-surface border border-border-default rounded shadow-lg">
156
+ <div className="flex items-center justify-between mb-2">
157
+ <span className="text-[11px] font-semibold uppercase tracking-widest text-text-muted">
158
+ {(selected || hovered)?.type}
159
+ </span>
160
+ {selected && (
161
+ <button
162
+ onClick={() => setSelected(null)}
163
+ aria-label="Close details"
164
+ className="text-text-muted hover:text-text-primary"
165
+ >
166
+ <Icon name="X" className="w-4 h-4" />
167
+ </button>
168
+ )}
169
+ </div>
170
+ <p className="text-sm text-text-primary break-words font-mono">{(selected || hovered)?.label}</p>
171
+ {selected && (
172
+ <p className="text-xs text-text-muted mt-2">{connected.length} connection(s)</p>
173
+ )}
174
+ </div>
175
+ )}
176
+ </div>
177
+ );
178
+ }
@@ -0,0 +1,95 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import type { ClientSession } from '../../../src/types';
3
+ import { Icon } from './ui/Icon';
4
+ import { formatDuration, formatTime, truncate } from '../../../src/lib/format';
5
+
6
+ interface Props {
7
+ sessions: ClientSession[];
8
+ selectedSessionId: string | null;
9
+ activeSessionId: string | undefined;
10
+ connection: 'connecting' | 'connected' | 'disconnected';
11
+ onSelect: (id: string) => void;
12
+ }
13
+
14
+ export function SessionSelector({
15
+ sessions,
16
+ selectedSessionId,
17
+ activeSessionId,
18
+ connection,
19
+ onSelect,
20
+ }: Props) {
21
+ const [open, setOpen] = useState(false);
22
+ const ref = useRef<HTMLDivElement>(null);
23
+
24
+ useEffect(() => {
25
+ function onDocClick(e: MouseEvent) {
26
+ if (!ref.current?.contains(e.target as Node)) setOpen(false);
27
+ }
28
+ document.addEventListener('click', onDocClick);
29
+ return () => document.removeEventListener('click', onDocClick);
30
+ }, []);
31
+
32
+ const current = sessions.find((s) => s.id === selectedSessionId);
33
+ const dotColor =
34
+ connection === 'connected'
35
+ ? 'bg-success'
36
+ : connection === 'connecting'
37
+ ? 'bg-warning'
38
+ : 'bg-error';
39
+
40
+ const label = current
41
+ ? (current.id === activeSessionId ? '● ' : '') + (current.skill?.toUpperCase() || truncate(current.id, 12))
42
+ : 'No session';
43
+
44
+ return (
45
+ <div ref={ref} className="relative">
46
+ <button
47
+ onClick={() => setOpen((v) => !v)}
48
+ aria-haspopup="listbox"
49
+ aria-expanded={open}
50
+ className="flex items-center gap-2 px-3 py-1.5 rounded-full border border-border-default bg-elevated text-text-secondary text-xs hover:border-accent transition-colors min-h-[36px]"
51
+ >
52
+ <span className={`w-2 h-2 rounded-full ${dotColor}`} />
53
+ <span>{label}</span>
54
+ <Icon name="CaretDown" className="w-4 h-4" />
55
+ </button>
56
+ {open && (
57
+ <ul
58
+ role="listbox"
59
+ className="absolute top-[calc(100%+8px)] right-0 min-w-[260px] max-h-[320px] overflow-y-auto bg-surface border border-border-default rounded z-50 shadow-lg p-1.5"
60
+ >
61
+ {sessions.map((s) => {
62
+ const duration = s.endedAt
63
+ ? `ended after ${formatDuration(s.endedAt - s.startTime)}`
64
+ : formatDuration(Date.now() - s.startTime);
65
+ const isActive = s.id === selectedSessionId;
66
+ return (
67
+ <li
68
+ key={s.id}
69
+ role="option"
70
+ aria-selected={isActive}
71
+ onClick={() => {
72
+ onSelect(s.id);
73
+ setOpen(false);
74
+ }}
75
+ className={`flex items-center gap-2 px-2.5 py-2 rounded cursor-pointer text-xs border-l-2 ${
76
+ isActive
77
+ ? 'bg-elevated border-accent text-text-primary'
78
+ : 'border-transparent text-text-secondary hover:bg-elevated'
79
+ }`}
80
+ >
81
+ <div className="flex flex-col min-w-0">
82
+ <span className="font-mono truncate">{truncate(s.id, 18)}</span>
83
+ <span className="text-text-muted text-[11px]">
84
+ {formatTime(s.startTime)} · {duration}
85
+ </span>
86
+ </div>
87
+ <span className="ml-auto text-text-muted uppercase">{s.source}</span>
88
+ </li>
89
+ );
90
+ })}
91
+ </ul>
92
+ )}
93
+ </div>
94
+ );
95
+ }