@grackle-ai/web-components 0.107.2

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 (279) hide show
  1. package/.rush/temp/3ae72563f781afd72723475938136f113846603e.untar.log +10 -0
  2. package/.rush/temp/bc1d5bf9201ce71abeaeaddd096deb9b0805d703.untar.log +10 -0
  3. package/.rush/temp/operation/_phase_build/all.log +18 -0
  4. package/.rush/temp/operation/_phase_build/log-chunks.jsonl +18 -0
  5. package/.rush/temp/operation/_phase_build/state.json +3 -0
  6. package/.rush/temp/operation/_phase_test/all.log +121 -0
  7. package/.rush/temp/operation/_phase_test/log-chunks.jsonl +121 -0
  8. package/.rush/temp/operation/_phase_test/state.json +3 -0
  9. package/.rush/temp/shrinkwrap-deps.json +938 -0
  10. package/.storybook/main.ts +22 -0
  11. package/.storybook/preview.tsx +30 -0
  12. package/config/rig.json +4 -0
  13. package/config/rush-project.json +12 -0
  14. package/dist/index.css +1 -0
  15. package/dist/index.js +39221 -0
  16. package/eslint.config.cjs +5 -0
  17. package/package.json +83 -0
  18. package/rush-logs/web-components._phase_build.cache.log +4 -0
  19. package/rush-logs/web-components._phase_test.cache.log +4 -0
  20. package/src/components/chat/ChatInput.module.scss +81 -0
  21. package/src/components/chat/ChatInput.stories.tsx +91 -0
  22. package/src/components/chat/ChatInput.tsx +168 -0
  23. package/src/components/chat/index.ts +6 -0
  24. package/src/components/dag/DagView.module.scss +149 -0
  25. package/src/components/dag/DagView.stories.tsx +125 -0
  26. package/src/components/dag/DagView.tsx +109 -0
  27. package/src/components/dag/TaskNode.stories.tsx +133 -0
  28. package/src/components/dag/TaskNode.tsx +40 -0
  29. package/src/components/dag/useDagLayout.ts +139 -0
  30. package/src/components/display/Breadcrumbs.module.scss +71 -0
  31. package/src/components/display/Breadcrumbs.stories.tsx +80 -0
  32. package/src/components/display/Breadcrumbs.tsx +46 -0
  33. package/src/components/display/Button.module.scss +110 -0
  34. package/src/components/display/Button.stories.tsx +88 -0
  35. package/src/components/display/Button.tsx +40 -0
  36. package/src/components/display/ConfirmDialog.module.scss +67 -0
  37. package/src/components/display/ConfirmDialog.stories.tsx +81 -0
  38. package/src/components/display/ConfirmDialog.tsx +88 -0
  39. package/src/components/display/CopyButton.module.scss +41 -0
  40. package/src/components/display/CopyButton.stories.tsx +78 -0
  41. package/src/components/display/CopyButton.tsx +64 -0
  42. package/src/components/display/DemoBanner.module.scss +37 -0
  43. package/src/components/display/DemoBanner.stories.tsx +40 -0
  44. package/src/components/display/DemoBanner.tsx +23 -0
  45. package/src/components/display/EventHoverRow.module.scss +102 -0
  46. package/src/components/display/EventHoverRow.stories.tsx +99 -0
  47. package/src/components/display/EventHoverRow.tsx +154 -0
  48. package/src/components/display/EventRenderer.module.scss +272 -0
  49. package/src/components/display/EventRenderer.stories.tsx +186 -0
  50. package/src/components/display/EventRenderer.tsx +271 -0
  51. package/src/components/display/EventStream.module.scss +93 -0
  52. package/src/components/display/EventStream.stories.tsx +249 -0
  53. package/src/components/display/EventStream.tsx +369 -0
  54. package/src/components/display/FloatingActionBar.module.scss +107 -0
  55. package/src/components/display/FloatingActionBar.stories.tsx +122 -0
  56. package/src/components/display/FloatingActionBar.tsx +119 -0
  57. package/src/components/display/SessionAttemptSelector.module.scss +50 -0
  58. package/src/components/display/SessionAttemptSelector.stories.tsx +78 -0
  59. package/src/components/display/SessionAttemptSelector.tsx +49 -0
  60. package/src/components/display/SessionPicker.module.scss +200 -0
  61. package/src/components/display/SessionPicker.stories.tsx +169 -0
  62. package/src/components/display/SessionPicker.tsx +214 -0
  63. package/src/components/display/Skeleton.module.scss +58 -0
  64. package/src/components/display/Skeleton.stories.tsx +94 -0
  65. package/src/components/display/Skeleton.tsx +127 -0
  66. package/src/components/display/Spinner.module.scss +41 -0
  67. package/src/components/display/Spinner.stories.tsx +66 -0
  68. package/src/components/display/Spinner.tsx +32 -0
  69. package/src/components/display/SplashScreen.module.scss +20 -0
  70. package/src/components/display/SplashScreen.stories.tsx +26 -0
  71. package/src/components/display/SplashScreen.tsx +16 -0
  72. package/src/components/display/SplitButton.module.scss +166 -0
  73. package/src/components/display/SplitButton.stories.tsx +95 -0
  74. package/src/components/display/SplitButton.tsx +128 -0
  75. package/src/components/display/Tooltip.module.scss +84 -0
  76. package/src/components/display/Tooltip.stories.tsx +240 -0
  77. package/src/components/display/Tooltip.tsx +184 -0
  78. package/src/components/display/extractText.test.tsx +48 -0
  79. package/src/components/display/index.ts +20 -0
  80. package/src/components/editable/EditableCheckbox.stories.tsx +54 -0
  81. package/src/components/editable/EditableCheckbox.tsx +39 -0
  82. package/src/components/editable/EditableField.module.scss +135 -0
  83. package/src/components/editable/EditableSelect.tsx +164 -0
  84. package/src/components/editable/EditableTextArea.stories.tsx +50 -0
  85. package/src/components/editable/EditableTextArea.tsx +148 -0
  86. package/src/components/editable/EditableTextField.stories.tsx +62 -0
  87. package/src/components/editable/EditableTextField.tsx +153 -0
  88. package/src/components/editable/EnvironmentSelect.module.scss +17 -0
  89. package/src/components/editable/EnvironmentSelect.stories.tsx +61 -0
  90. package/src/components/editable/EnvironmentSelect.tsx +87 -0
  91. package/src/components/editable/index.ts +13 -0
  92. package/src/components/editable/useEditableField.test.tsx +233 -0
  93. package/src/components/editable/useEditableField.ts +173 -0
  94. package/src/components/index.ts +20 -0
  95. package/src/components/knowledge/KnowledgeDetailPanel.module.scss +162 -0
  96. package/src/components/knowledge/KnowledgeDetailPanel.stories.tsx +208 -0
  97. package/src/components/knowledge/KnowledgeDetailPanel.tsx +122 -0
  98. package/src/components/knowledge/KnowledgeGraph.module.scss +110 -0
  99. package/src/components/knowledge/KnowledgeGraph.stories.tsx +180 -0
  100. package/src/components/knowledge/KnowledgeGraph.tsx +455 -0
  101. package/src/components/knowledge/KnowledgeNav.module.scss +130 -0
  102. package/src/components/knowledge/KnowledgeNav.stories.tsx +108 -0
  103. package/src/components/knowledge/KnowledgeNav.tsx +138 -0
  104. package/src/components/knowledge/index.ts +3 -0
  105. package/src/components/layout/AppNav.module.scss +82 -0
  106. package/src/components/layout/AppNav.stories.tsx +115 -0
  107. package/src/components/layout/AppNav.tsx +133 -0
  108. package/src/components/layout/BottomStatusBar.module.scss +58 -0
  109. package/src/components/layout/BottomStatusBar.stories.tsx +35 -0
  110. package/src/components/layout/BottomStatusBar.tsx +206 -0
  111. package/src/components/layout/Sidebar.module.scss +60 -0
  112. package/src/components/layout/Sidebar.stories.tsx +46 -0
  113. package/src/components/layout/Sidebar.tsx +84 -0
  114. package/src/components/layout/StatusBar.module.scss +108 -0
  115. package/src/components/layout/StatusBar.stories.tsx +119 -0
  116. package/src/components/layout/StatusBar.tsx +70 -0
  117. package/src/components/layout/index.ts +9 -0
  118. package/src/components/lists/EnvironmentNav.module.scss +118 -0
  119. package/src/components/lists/EnvironmentNav.stories.tsx +121 -0
  120. package/src/components/lists/EnvironmentNav.tsx +133 -0
  121. package/src/components/lists/FindingsNav.module.scss +126 -0
  122. package/src/components/lists/FindingsNav.tsx +146 -0
  123. package/src/components/lists/TaskList.module.scss +206 -0
  124. package/src/components/lists/TaskList.stories.tsx +401 -0
  125. package/src/components/lists/TaskList.tsx +509 -0
  126. package/src/components/lists/index.ts +6 -0
  127. package/src/components/lists/listHelpers.tsx +130 -0
  128. package/src/components/notifications/Callout.module.scss +83 -0
  129. package/src/components/notifications/Callout.stories.tsx +81 -0
  130. package/src/components/notifications/Callout.tsx +64 -0
  131. package/src/components/notifications/Toast.module.scss +86 -0
  132. package/src/components/notifications/Toast.stories.tsx +71 -0
  133. package/src/components/notifications/Toast.tsx +51 -0
  134. package/src/components/notifications/ToastContainer.module.scss +23 -0
  135. package/src/components/notifications/ToastContainer.stories.tsx +66 -0
  136. package/src/components/notifications/ToastContainer.tsx +29 -0
  137. package/src/components/notifications/UpdateBanner.stories.tsx +77 -0
  138. package/src/components/notifications/UpdateBanner.test.tsx +64 -0
  139. package/src/components/notifications/UpdateBanner.tsx +44 -0
  140. package/src/components/notifications/index.ts +8 -0
  141. package/src/components/panels/AboutPanel.stories.tsx +70 -0
  142. package/src/components/panels/AboutPanel.tsx +66 -0
  143. package/src/components/panels/AppearancePanel.stories.tsx +45 -0
  144. package/src/components/panels/AppearancePanel.tsx +97 -0
  145. package/src/components/panels/CredentialProvidersPanel.stories.tsx +62 -0
  146. package/src/components/panels/CredentialProvidersPanel.tsx +111 -0
  147. package/src/components/panels/EnvironmentEditPanel.module.scss +170 -0
  148. package/src/components/panels/EnvironmentEditPanel.stories.tsx +206 -0
  149. package/src/components/panels/EnvironmentEditPanel.tsx +785 -0
  150. package/src/components/panels/FindingsPanel.module.scss +94 -0
  151. package/src/components/panels/FindingsPanel.stories.tsx +109 -0
  152. package/src/components/panels/FindingsPanel.tsx +76 -0
  153. package/src/components/panels/KeyboardShortcutsPanel.module.scss +65 -0
  154. package/src/components/panels/KeyboardShortcutsPanel.stories.tsx +40 -0
  155. package/src/components/panels/KeyboardShortcutsPanel.tsx +104 -0
  156. package/src/components/panels/PluginsPanel.tsx +77 -0
  157. package/src/components/panels/SettingsPanel.module.scss +336 -0
  158. package/src/components/panels/TaskActionButtons.module.scss +22 -0
  159. package/src/components/panels/TaskActionButtons.stories.tsx +125 -0
  160. package/src/components/panels/TaskActionButtons.tsx +87 -0
  161. package/src/components/panels/TaskEditPanel.module.scss +202 -0
  162. package/src/components/panels/TaskEditPanel.stories.tsx +75 -0
  163. package/src/components/panels/TaskEditPanel.tsx +328 -0
  164. package/src/components/panels/TaskOverviewPanel.module.scss +236 -0
  165. package/src/components/panels/TaskOverviewPanel.stories.tsx +219 -0
  166. package/src/components/panels/TaskOverviewPanel.tsx +270 -0
  167. package/src/components/panels/TokensPanel.stories.tsx +131 -0
  168. package/src/components/panels/TokensPanel.tsx +143 -0
  169. package/src/components/panels/WorkpadPanel.module.scss +39 -0
  170. package/src/components/panels/WorkpadPanel.stories.tsx +56 -0
  171. package/src/components/panels/WorkpadPanel.tsx +63 -0
  172. package/src/components/panels/index.ts +13 -0
  173. package/src/components/personas/McpToolSelector.module.scss +109 -0
  174. package/src/components/personas/McpToolSelector.stories.tsx +129 -0
  175. package/src/components/personas/McpToolSelector.tsx +180 -0
  176. package/src/components/personas/PersonaManager.module.scss +233 -0
  177. package/src/components/personas/PersonaManager.stories.tsx +139 -0
  178. package/src/components/personas/PersonaManager.tsx +122 -0
  179. package/src/components/schedules/ScheduleManager.module.scss +98 -0
  180. package/src/components/schedules/ScheduleManager.stories.tsx +78 -0
  181. package/src/components/schedules/ScheduleManager.tsx +160 -0
  182. package/src/components/settings/SettingsNav.module.scss +82 -0
  183. package/src/components/settings/SettingsNav.stories.tsx +83 -0
  184. package/src/components/settings/SettingsNav.tsx +104 -0
  185. package/src/components/streams/StreamDetailPanel.module.scss +206 -0
  186. package/src/components/streams/StreamDetailPanel.stories.tsx +132 -0
  187. package/src/components/streams/StreamDetailPanel.tsx +119 -0
  188. package/src/components/streams/StreamList.module.scss +92 -0
  189. package/src/components/streams/StreamList.stories.tsx +99 -0
  190. package/src/components/streams/StreamList.tsx +114 -0
  191. package/src/components/streams/index.ts +10 -0
  192. package/src/components/tools/AgentToolCard.module.scss +118 -0
  193. package/src/components/tools/AgentToolCard.stories.tsx +304 -0
  194. package/src/components/tools/AgentToolCard.tsx +247 -0
  195. package/src/components/tools/FileEditCard.stories.tsx +138 -0
  196. package/src/components/tools/FileEditCard.tsx +160 -0
  197. package/src/components/tools/FileReadCard.stories.tsx +120 -0
  198. package/src/components/tools/FileReadCard.tsx +106 -0
  199. package/src/components/tools/FindingCard.stories.tsx +124 -0
  200. package/src/components/tools/FindingCard.tsx +178 -0
  201. package/src/components/tools/GenericToolCard.stories.tsx +80 -0
  202. package/src/components/tools/GenericToolCard.tsx +111 -0
  203. package/src/components/tools/IpcCard.stories.tsx +129 -0
  204. package/src/components/tools/IpcCard.tsx +178 -0
  205. package/src/components/tools/KnowledgeCard.stories.tsx +112 -0
  206. package/src/components/tools/KnowledgeCard.tsx +165 -0
  207. package/src/components/tools/MetadataCard.stories.tsx +32 -0
  208. package/src/components/tools/MetadataCard.tsx +39 -0
  209. package/src/components/tools/SearchCard.stories.tsx +74 -0
  210. package/src/components/tools/SearchCard.tsx +86 -0
  211. package/src/components/tools/ShellCard.stories.tsx +112 -0
  212. package/src/components/tools/ShellCard.tsx +106 -0
  213. package/src/components/tools/TaskCard.stories.tsx +123 -0
  214. package/src/components/tools/TaskCard.tsx +203 -0
  215. package/src/components/tools/TodoCard.module.scss +131 -0
  216. package/src/components/tools/TodoCard.stories.tsx +202 -0
  217. package/src/components/tools/TodoCard.tsx +200 -0
  218. package/src/components/tools/ToolCard.stories.tsx +177 -0
  219. package/src/components/tools/ToolCard.tsx +60 -0
  220. package/src/components/tools/ToolCardProps.ts +20 -0
  221. package/src/components/tools/ToolSearchCard.stories.tsx +81 -0
  222. package/src/components/tools/ToolSearchCard.tsx +86 -0
  223. package/src/components/tools/WorkpadCard.stories.tsx +106 -0
  224. package/src/components/tools/WorkpadCard.tsx +125 -0
  225. package/src/components/tools/classifyTool.test.ts +44 -0
  226. package/src/components/tools/classifyTool.ts +134 -0
  227. package/src/components/tools/parseDiff.ts +95 -0
  228. package/src/components/tools/parseShellOutput.ts +28 -0
  229. package/src/components/tools/toolCardHelpers.test.ts +53 -0
  230. package/src/components/tools/toolCards.module.scss +234 -0
  231. package/src/components/workspace/WorkspaceBoard.module.scss +238 -0
  232. package/src/components/workspace/WorkspaceBoard.stories.tsx +240 -0
  233. package/src/components/workspace/WorkspaceBoard.tsx +232 -0
  234. package/src/components/workspace/WorkspaceFormFields.module.scss +79 -0
  235. package/src/components/workspace/WorkspaceFormFields.stories.tsx +133 -0
  236. package/src/components/workspace/WorkspaceFormFields.tsx +185 -0
  237. package/src/context/GrackleContext.ts +28 -0
  238. package/src/context/GrackleContextTypes.ts +64 -0
  239. package/src/context/SidebarContext.tsx +53 -0
  240. package/src/context/ThemeContext.tsx +21 -0
  241. package/src/context/ToastContext.tsx +56 -0
  242. package/src/hooks/types.ts +864 -0
  243. package/src/hooks/useEventSelection.test.ts +204 -0
  244. package/src/hooks/useEventSelection.ts +158 -0
  245. package/src/hooks/useSmartScroll.ts +151 -0
  246. package/src/hooks/useTheme.ts +228 -0
  247. package/src/index.ts +210 -0
  248. package/src/mocks/MockGrackleProvider.tsx +1397 -0
  249. package/src/mocks/mockData.ts +1966 -0
  250. package/src/mocks/mockKnowledgeData.ts +294 -0
  251. package/src/scss.d.ts +12 -0
  252. package/src/styles/global.scss +244 -0
  253. package/src/styles/mixins.scss +278 -0
  254. package/src/styles/prism-theme.scss +148 -0
  255. package/src/styles/theme.scss +1102 -0
  256. package/src/test-utils/storybook-decorators.tsx +50 -0
  257. package/src/test-utils/storybook-helpers.ts +262 -0
  258. package/src/themes.ts +142 -0
  259. package/src/utils/boardColumns.ts +141 -0
  260. package/src/utils/breadcrumbs.test.ts +285 -0
  261. package/src/utils/breadcrumbs.ts +222 -0
  262. package/src/utils/dashboard.test.ts +156 -0
  263. package/src/utils/dashboard.ts +195 -0
  264. package/src/utils/eventContent.test.ts +353 -0
  265. package/src/utils/eventContent.ts +209 -0
  266. package/src/utils/findingCategory.ts +33 -0
  267. package/src/utils/format.ts +27 -0
  268. package/src/utils/iconSize.ts +18 -0
  269. package/src/utils/navigation.ts +205 -0
  270. package/src/utils/route-config.test.ts +128 -0
  271. package/src/utils/scrollUtils.test.ts +65 -0
  272. package/src/utils/scrollUtils.ts +49 -0
  273. package/src/utils/sessionEvents.test.ts +302 -0
  274. package/src/utils/sessionEvents.ts +233 -0
  275. package/src/utils/taskStatus.tsx +137 -0
  276. package/src/utils/time.ts +92 -0
  277. package/tsconfig.json +8 -0
  278. package/vite.config.ts +20 -0
  279. package/vitest.config.ts +10 -0
@@ -0,0 +1,63 @@
1
+ import type { JSX } from "react";
2
+ import styles from "./WorkpadPanel.module.scss";
3
+
4
+ /** Props for the WorkpadPanel component. */
5
+ export interface WorkpadPanelProps {
6
+ /** Raw JSON string from the task's workpad field. */
7
+ workpad: string;
8
+ }
9
+
10
+ /** Parsed workpad shape. */
11
+ interface ParsedWorkpad {
12
+ status?: string;
13
+ summary?: string;
14
+ extra?: Record<string, unknown>;
15
+ }
16
+
17
+ /**
18
+ * Displays a task's workpad (persistent structured context).
19
+ * Pure presentational — accepts the raw JSON string and renders
20
+ * status, summary, and extra fields if present.
21
+ */
22
+ export function WorkpadPanel({ workpad }: WorkpadPanelProps): JSX.Element | undefined {
23
+ if (!workpad) {
24
+ return undefined;
25
+ }
26
+
27
+ let parsed: ParsedWorkpad;
28
+ try {
29
+ const raw: unknown = JSON.parse(workpad);
30
+ if (raw === undefined || raw === null || typeof raw !== "object" || Array.isArray(raw)) {
31
+ return undefined;
32
+ }
33
+ parsed = raw as ParsedWorkpad;
34
+ } catch {
35
+ return (
36
+ <div className={styles.workpadSection} data-testid="workpad-panel">
37
+ <div className={styles.workpadLabel}>Workpad</div>
38
+ <div className={styles.workpadExtra}>{workpad}</div>
39
+ </div>
40
+ );
41
+ }
42
+
43
+ if (!parsed.status && !parsed.summary && !parsed.extra) {
44
+ return undefined;
45
+ }
46
+
47
+ return (
48
+ <div className={styles.workpadSection} data-testid="workpad-panel">
49
+ <div className={styles.workpadLabel}>Workpad</div>
50
+ {parsed.status && (
51
+ <div className={styles.workpadStatus} data-testid="workpad-status">{parsed.status}</div>
52
+ )}
53
+ {parsed.summary && (
54
+ <div className={styles.workpadSummary} data-testid="workpad-summary">{parsed.summary}</div>
55
+ )}
56
+ {parsed.extra && Object.keys(parsed.extra).length > 0 && (
57
+ <div className={styles.workpadExtra} data-testid="workpad-extra">
58
+ {JSON.stringify(parsed.extra, null, 2)}
59
+ </div>
60
+ )}
61
+ </div>
62
+ );
63
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Main content panel components.
3
+ * @module panels
4
+ */
5
+ export { FindingsPanel } from "./FindingsPanel.js";
6
+ export { TokensPanel } from "./TokensPanel.js";
7
+ export { AppearancePanel } from "./AppearancePanel.js";
8
+ export { AboutPanel } from "./AboutPanel.js";
9
+ export { TaskEditPanel } from "./TaskEditPanel.js";
10
+ export { TaskActionButtons } from "./TaskActionButtons.js";
11
+ export { TaskOverviewPanel } from "./TaskOverviewPanel.js";
12
+ export { PluginsPanel } from "./PluginsPanel.js";
13
+ export type { PluginsPanelProps } from "./PluginsPanel.js";
@@ -0,0 +1,109 @@
1
+ .container {
2
+ display: flex;
3
+ flex-direction: column;
4
+ gap: 0.5rem;
5
+ }
6
+
7
+ .header {
8
+ display: flex;
9
+ justify-content: space-between;
10
+ align-items: center;
11
+ }
12
+
13
+ .count {
14
+ font-size: 0.85rem;
15
+ color: var(--text-secondary, #888);
16
+ }
17
+
18
+ .presets {
19
+ display: flex;
20
+ gap: 0.375rem;
21
+ flex-wrap: wrap;
22
+ }
23
+
24
+ .presetButton {
25
+ padding: 0.25rem 0.625rem;
26
+ border: 1px solid var(--border-color, #444);
27
+ border-radius: 4px;
28
+ background: var(--bg-secondary, #2a2a2a);
29
+ color: var(--text-primary, #ddd);
30
+ cursor: pointer;
31
+ font-size: 0.8rem;
32
+
33
+ &:hover:not(:disabled) {
34
+ background: var(--bg-hover, #333);
35
+ }
36
+
37
+ &:disabled {
38
+ opacity: 0.5;
39
+ cursor: not-allowed;
40
+ }
41
+ }
42
+
43
+ .filterInput {
44
+ padding: 0.375rem 0.5rem;
45
+ border: 1px solid var(--border-color, #444);
46
+ border-radius: 4px;
47
+ background: var(--bg-secondary, #2a2a2a);
48
+ color: var(--text-primary, #ddd);
49
+ font-size: 0.85rem;
50
+
51
+ &:disabled {
52
+ opacity: 0.5;
53
+ }
54
+ }
55
+
56
+ .groups {
57
+ display: flex;
58
+ flex-direction: column;
59
+ gap: 0.5rem;
60
+ max-height: 400px;
61
+ overflow-y: auto;
62
+ padding-right: 0.25rem;
63
+ }
64
+
65
+ .group {
66
+ border: 1px solid var(--border-color, #333);
67
+ border-radius: 4px;
68
+ padding: 0.375rem 0.5rem;
69
+ }
70
+
71
+ .groupHeader {
72
+ display: flex;
73
+ align-items: center;
74
+ gap: 0.375rem;
75
+ cursor: pointer;
76
+ font-weight: 600;
77
+ font-size: 0.85rem;
78
+ }
79
+
80
+ .groupName {
81
+ text-transform: capitalize;
82
+ }
83
+
84
+ .groupCount {
85
+ font-weight: 400;
86
+ color: var(--text-secondary, #888);
87
+ font-size: 0.75rem;
88
+ }
89
+
90
+ .toolList {
91
+ display: flex;
92
+ flex-wrap: wrap;
93
+ gap: 0.125rem 0.75rem;
94
+ margin-top: 0.25rem;
95
+ padding-left: 1.25rem;
96
+ }
97
+
98
+ .toolItem {
99
+ display: flex;
100
+ align-items: center;
101
+ gap: 0.25rem;
102
+ cursor: pointer;
103
+ font-size: 0.8rem;
104
+ }
105
+
106
+ .toolName {
107
+ font-family: var(--font-mono, monospace);
108
+ font-size: 0.75rem;
109
+ }
@@ -0,0 +1,129 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { expect, fn, userEvent, within } from "@storybook/test";
3
+ import { McpToolSelector } from "./McpToolSelector.js";
4
+ import {
5
+ DEFAULT_SCOPED_MCP_TOOLS,
6
+ WORKER_MCP_TOOLS,
7
+ ALL_MCP_TOOL_NAMES,
8
+ } from "@grackle-ai/common";
9
+
10
+ const meta: Meta<typeof McpToolSelector> = {
11
+ title: "Grackle/Personas/McpToolSelector",
12
+ tags: ["autodocs"],
13
+ component: McpToolSelector,
14
+ args: {
15
+ selectedTools: [],
16
+ onChange: fn(),
17
+ disabled: false,
18
+ },
19
+ };
20
+
21
+ export default meta;
22
+ type Story = StoryObj<typeof McpToolSelector>;
23
+
24
+ export const EmptySelection: Story = {
25
+ play: async ({ canvasElement }) => {
26
+ const canvas = within(canvasElement);
27
+ // Verify preset buttons are visible
28
+ await expect(canvas.getByTestId("preset-default")).toBeInTheDocument();
29
+ await expect(canvas.getByTestId("preset-worker")).toBeInTheDocument();
30
+ await expect(canvas.getByTestId("preset-orchestrator")).toBeInTheDocument();
31
+ await expect(canvas.getByTestId("preset-admin")).toBeInTheDocument();
32
+ // Verify filter input is visible
33
+ await expect(canvas.getByTestId("mcp-tool-filter")).toBeInTheDocument();
34
+ // Verify "Using default" text (count is dynamic from DEFAULT_SCOPED_MCP_TOOLS)
35
+ await expect(canvas.getByText(/Using default \(\d+ tools\)/)).toBeInTheDocument();
36
+ },
37
+ };
38
+
39
+ export const WithPresetDefault: Story = {
40
+ play: async ({ canvasElement, args }) => {
41
+ const canvas = within(canvasElement);
42
+ await userEvent.click(canvas.getByTestId("preset-default"));
43
+ await expect(args.onChange).toHaveBeenCalledWith(
44
+ expect.arrayContaining([...DEFAULT_SCOPED_MCP_TOOLS]),
45
+ );
46
+ },
47
+ };
48
+
49
+ export const WithPresetWorker: Story = {
50
+ play: async ({ canvasElement, args }) => {
51
+ const canvas = within(canvasElement);
52
+ await userEvent.click(canvas.getByTestId("preset-worker"));
53
+ await expect(args.onChange).toHaveBeenCalledWith(
54
+ expect.arrayContaining([...WORKER_MCP_TOOLS]),
55
+ );
56
+ },
57
+ };
58
+
59
+ export const CustomSelection: Story = {
60
+ args: {
61
+ selectedTools: ["finding_post", "task_list", "workpad_read"],
62
+ },
63
+ play: async ({ canvasElement }) => {
64
+ const canvas = within(canvasElement);
65
+ // Verify selected tools are checked
66
+ const findingPost = canvas.getByTestId("tool-finding_post") as HTMLInputElement;
67
+ await expect(findingPost.checked).toBe(true);
68
+ const taskList = canvas.getByTestId("tool-task_list") as HTMLInputElement;
69
+ await expect(taskList.checked).toBe(true);
70
+ // Verify unselected tool is not checked
71
+ const envList = canvas.getByTestId("tool-env_list") as HTMLInputElement;
72
+ await expect(envList.checked).toBe(false);
73
+ // Verify count display
74
+ await expect(canvas.getByText(`3 of ${ALL_MCP_TOOL_NAMES.size} tools selected`)).toBeInTheDocument();
75
+ },
76
+ };
77
+
78
+ export const ToggleIndividualTool: Story = {
79
+ play: async ({ canvasElement, args }) => {
80
+ const canvas = within(canvasElement);
81
+ await userEvent.click(canvas.getByTestId("tool-finding_post"));
82
+ await expect(args.onChange).toHaveBeenCalledWith(["finding_post"]);
83
+ },
84
+ };
85
+
86
+ export const SearchFilter: Story = {
87
+ play: async ({ canvasElement }) => {
88
+ const canvas = within(canvasElement);
89
+ const filterInput = canvas.getByTestId("mcp-tool-filter");
90
+ await userEvent.type(filterInput, "finding");
91
+ // finding group should be visible with its tools
92
+ await expect(canvas.getByTestId("tool-group-finding")).toBeInTheDocument();
93
+ await expect(canvas.getByTestId("tool-finding_post")).toBeInTheDocument();
94
+ await expect(canvas.getByTestId("tool-finding_list")).toBeInTheDocument();
95
+ // env group should be hidden (no match)
96
+ await expect(canvas.queryByTestId("tool-group-env")).not.toBeInTheDocument();
97
+ },
98
+ };
99
+
100
+ export const GroupSelectAll: Story = {
101
+ play: async ({ canvasElement, args }) => {
102
+ const canvas = within(canvasElement);
103
+ // Click the "task" group toggle to select all task tools
104
+ await userEvent.click(canvas.getByTestId("group-toggle-task"));
105
+ const called = (args.onChange as ReturnType<typeof fn>).mock.calls[0][0] as string[];
106
+ // Should include all task tools
107
+ await expect(called).toContain("task_list");
108
+ await expect(called).toContain("task_create");
109
+ await expect(called).toContain("task_show");
110
+ await expect(called).toContain("task_start");
111
+ await expect(called).toContain("task_complete");
112
+ },
113
+ };
114
+
115
+ export const DisabledState: Story = {
116
+ args: {
117
+ disabled: true,
118
+ selectedTools: ["finding_post"],
119
+ },
120
+ play: async ({ canvasElement }) => {
121
+ const canvas = within(canvasElement);
122
+ // Preset buttons should be disabled
123
+ await expect(canvas.getByTestId("preset-default")).toBeDisabled();
124
+ // Filter input should be disabled
125
+ await expect(canvas.getByTestId("mcp-tool-filter")).toBeDisabled();
126
+ // Tool checkboxes should be disabled
127
+ await expect(canvas.getByTestId("tool-finding_post")).toBeDisabled();
128
+ },
129
+ };
@@ -0,0 +1,180 @@
1
+ /**
2
+ * A categorized multiselect for choosing which MCP tools a persona can access.
3
+ *
4
+ * Pure presentational component — receives data and callbacks as props.
5
+ */
6
+
7
+ import { useState, useMemo, useCallback, type JSX } from "react";
8
+ import {
9
+ ALL_MCP_TOOL_NAMES,
10
+ DEFAULT_SCOPED_MCP_TOOLS,
11
+ WORKER_MCP_TOOLS,
12
+ ORCHESTRATOR_MCP_TOOLS,
13
+ ADMIN_MCP_TOOLS,
14
+ } from "@grackle-ai/common";
15
+ import styles from "./McpToolSelector.module.scss";
16
+
17
+ /** Tool groups derived from the naming convention (prefix before first underscore). */
18
+ const TOOL_GROUPS: { group: string; tools: string[] }[] = (() => {
19
+ const grouped = new Map<string, string[]>();
20
+ for (const name of ALL_MCP_TOOL_NAMES) {
21
+ // Most tools use "group_action" naming; "get_version_status" is a special case
22
+ const group = name === "get_version_status" ? "version" : name.split("_")[0];
23
+ if (!grouped.has(group)) {
24
+ grouped.set(group, []);
25
+ }
26
+ grouped.get(group)!.push(name);
27
+ }
28
+ // Sort groups alphabetically, tools within each group alphabetically
29
+ return [...grouped.entries()]
30
+ .sort(([a], [b]) => a.localeCompare(b))
31
+ .map(([group, tools]) => ({ group, tools: tools.sort() }));
32
+ })();
33
+
34
+ /** Preset definitions for quick selection. */
35
+ const PRESETS = [
36
+ { name: "default", label: "Default", tools: DEFAULT_SCOPED_MCP_TOOLS },
37
+ { name: "worker", label: "Worker", tools: WORKER_MCP_TOOLS },
38
+ { name: "orchestrator", label: "Orchestrator", tools: ORCHESTRATOR_MCP_TOOLS },
39
+ { name: "admin", label: "Admin", tools: ADMIN_MCP_TOOLS },
40
+ ] as const;
41
+
42
+ export interface McpToolSelectorProps {
43
+ /** Currently selected tool names. Empty = default (shown as hint, not checked). */
44
+ selectedTools: string[];
45
+ /** Callback when selection changes. */
46
+ onChange: (tools: string[]) => void;
47
+ /** Whether the component is disabled. */
48
+ disabled?: boolean;
49
+ }
50
+
51
+ export function McpToolSelector({ selectedTools, onChange, disabled }: McpToolSelectorProps): JSX.Element {
52
+ const [filter, setFilter] = useState("");
53
+ const selectedSet = useMemo(() => new Set(selectedTools), [selectedTools]);
54
+
55
+ const toggleTool = useCallback((tool: string) => {
56
+ if (disabled) {
57
+ return;
58
+ }
59
+ const next = new Set(selectedSet);
60
+ if (next.has(tool)) {
61
+ next.delete(tool);
62
+ } else {
63
+ next.add(tool);
64
+ }
65
+ onChange([...next].sort());
66
+ }, [selectedSet, onChange, disabled]);
67
+
68
+ const toggleGroup = useCallback((tools: string[], allSelected: boolean) => {
69
+ if (disabled) {
70
+ return;
71
+ }
72
+ const next = new Set(selectedSet);
73
+ for (const t of tools) {
74
+ if (allSelected) {
75
+ next.delete(t);
76
+ } else {
77
+ next.add(t);
78
+ }
79
+ }
80
+ onChange([...next].sort());
81
+ }, [selectedSet, onChange, disabled]);
82
+
83
+ const applyPreset = useCallback((tools: readonly string[]) => {
84
+ if (disabled) {
85
+ return;
86
+ }
87
+ onChange([...tools].sort());
88
+ }, [onChange, disabled]);
89
+
90
+ const lowerFilter = filter.toLowerCase();
91
+
92
+ return (
93
+ <div className={styles.container} data-testid="mcp-tool-selector">
94
+ <div className={styles.header}>
95
+ <span className={styles.count}>
96
+ {selectedTools.length === 0
97
+ ? `Using default (${DEFAULT_SCOPED_MCP_TOOLS.length} tools)`
98
+ : `${selectedTools.length} of ${ALL_MCP_TOOL_NAMES.size} tools selected`}
99
+ </span>
100
+ </div>
101
+
102
+ <div className={styles.presets} data-testid="mcp-tool-presets">
103
+ {PRESETS.map((preset) => (
104
+ <button
105
+ key={preset.name}
106
+ type="button"
107
+ className={styles.presetButton}
108
+ disabled={disabled}
109
+ onClick={() => applyPreset(preset.tools)}
110
+ data-testid={`preset-${preset.name}`}
111
+ >
112
+ {preset.label}
113
+ </button>
114
+ ))}
115
+ <button
116
+ type="button"
117
+ className={styles.presetButton}
118
+ disabled={disabled}
119
+ onClick={() => onChange([])}
120
+ data-testid="preset-clear"
121
+ >
122
+ Clear
123
+ </button>
124
+ </div>
125
+
126
+ <input
127
+ type="text"
128
+ className={styles.filterInput}
129
+ placeholder="Filter tools..."
130
+ value={filter}
131
+ onChange={(e) => setFilter(e.target.value)}
132
+ disabled={disabled}
133
+ data-testid="mcp-tool-filter"
134
+ />
135
+
136
+ <div className={styles.groups}>
137
+ {TOOL_GROUPS.map(({ group, tools }) => {
138
+ const visibleTools = lowerFilter
139
+ ? tools.filter((t) => t.toLowerCase().includes(lowerFilter))
140
+ : tools;
141
+ if (visibleTools.length === 0) {
142
+ return null;
143
+ }
144
+ const allSelected = visibleTools.every((t) => selectedSet.has(t));
145
+ return (
146
+ <div key={group} className={styles.group} data-testid={`tool-group-${group}`}>
147
+ <label className={styles.groupHeader}>
148
+ <input
149
+ type="checkbox"
150
+ checked={allSelected}
151
+ onChange={() => toggleGroup(visibleTools, allSelected)}
152
+ disabled={disabled}
153
+ data-testid={`group-toggle-${group}`}
154
+ />
155
+ <span className={styles.groupName}>{group}</span>
156
+ <span className={styles.groupCount}>
157
+ ({visibleTools.filter((t) => selectedSet.has(t)).length}/{visibleTools.length})
158
+ </span>
159
+ </label>
160
+ <div className={styles.toolList}>
161
+ {visibleTools.map((tool) => (
162
+ <label key={tool} className={styles.toolItem}>
163
+ <input
164
+ type="checkbox"
165
+ checked={selectedSet.has(tool)}
166
+ onChange={() => toggleTool(tool)}
167
+ disabled={disabled}
168
+ data-testid={`tool-${tool}`}
169
+ />
170
+ <span className={styles.toolName}>{tool}</span>
171
+ </label>
172
+ ))}
173
+ </div>
174
+ </div>
175
+ );
176
+ })}
177
+ </div>
178
+ </div>
179
+ );
180
+ }