@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,200 @@
1
+ import { type JSX, type ReactNode } from "react";
2
+ import { Check, Circle, ListChecks } from "lucide-react";
3
+ import type { ToolCardProps } from "./ToolCardProps.js";
4
+ import { ICON_SM, ICON_MD } from "../../utils/iconSize.js";
5
+ import styles from "./toolCards.module.scss";
6
+ import todoStyles from "./TodoCard.module.scss";
7
+
8
+ /** A normalized todo item used for rendering. */
9
+ interface TodoItem {
10
+ /** Display text (e.g. "Get bread"). */
11
+ content: string;
12
+ /** Present-tense description shown when in-progress (e.g. "Getting bread"). */
13
+ activeForm?: string;
14
+ /** Lifecycle status. */
15
+ status: "pending" | "in_progress" | "completed";
16
+ }
17
+
18
+ /** Checkbox pattern for Goose's markdown checklist: `- [x]`, `- [ ]`, `- [~]`, `- [/]` */
19
+ const CHECKBOX_PATTERN: RegExp = /^[-*]\s*\[([ xX~!/])\]\s*(.+)$/;
20
+
21
+ /**
22
+ * Parses todo items from args across all supported runtimes.
23
+ *
24
+ * Handles three formats:
25
+ * - Claude Code TodoWrite: `{ todos: [{ content, activeForm, status }] }`
26
+ * - Codex update_plan: `{ plan: [{ step, status }] }`
27
+ * - Goose todo_write: `{ content: "markdown checklist" }`
28
+ */
29
+ function parseTodos(args: unknown): TodoItem[] {
30
+ if (args === undefined || typeof args !== "object" || args === null) {
31
+ return [];
32
+ }
33
+ const a = args as Record<string, unknown>;
34
+
35
+ // Claude Code: { todos: [...] }
36
+ if (Array.isArray(a.todos)) {
37
+ return (a.todos as unknown[])
38
+ .filter(
39
+ (item): item is Record<string, unknown> =>
40
+ typeof item === "object" &&
41
+ item !== null &&
42
+ typeof (item as Record<string, unknown>).content === "string" &&
43
+ typeof (item as Record<string, unknown>).status === "string",
44
+ )
45
+ .map((item) => ({
46
+ content: item.content as string,
47
+ activeForm: typeof item.activeForm === "string" ? item.activeForm : undefined,
48
+ status: normalizeStatus(item.status as string),
49
+ }));
50
+ }
51
+
52
+ // Codex: { plan: [{ step, status }] }
53
+ if (Array.isArray(a.plan)) {
54
+ return (a.plan as unknown[])
55
+ .filter(
56
+ (item): item is Record<string, unknown> =>
57
+ typeof item === "object" &&
58
+ item !== null &&
59
+ typeof (item as Record<string, unknown>).step === "string" &&
60
+ typeof (item as Record<string, unknown>).status === "string",
61
+ )
62
+ .map((item) => ({
63
+ content: item.step as string,
64
+ status: normalizeStatus(item.status as string),
65
+ }));
66
+ }
67
+
68
+ // Goose: { content: "markdown checklist" }
69
+ if (typeof a.content === "string") {
70
+ return parseMarkdownChecklist(a.content);
71
+ }
72
+
73
+ return [];
74
+ }
75
+
76
+ /** Normalizes status strings across runtimes to our three canonical values. */
77
+ function normalizeStatus(status: string): "pending" | "in_progress" | "completed" {
78
+ switch (status.toLowerCase()) {
79
+ case "completed":
80
+ case "done":
81
+ case "complete":
82
+ return "completed";
83
+ case "in_progress":
84
+ case "in-progress":
85
+ case "working":
86
+ case "active":
87
+ return "in_progress";
88
+ default:
89
+ return "pending";
90
+ }
91
+ }
92
+
93
+ /** Parses a markdown checklist string (Goose format) into TodoItems. */
94
+ function parseMarkdownChecklist(content: string): TodoItem[] {
95
+ const lines: string[] = content.split("\n");
96
+ const items: TodoItem[] = [];
97
+ for (const line of lines) {
98
+ const match: RegExpExecArray | null = CHECKBOX_PATTERN.exec(line.trim());
99
+ if (match) {
100
+ const marker: string = match[1];
101
+ const text: string = match[2].trim();
102
+ let status: "pending" | "in_progress" | "completed" = "pending";
103
+ if (marker === "x" || marker === "X") {
104
+ status = "completed";
105
+ } else if (marker === "~" || marker === "/" || marker === "!") {
106
+ status = "in_progress";
107
+ }
108
+ items.push({ content: text, status });
109
+ }
110
+ }
111
+ return items;
112
+ }
113
+
114
+ /** Status icon for a todo item. */
115
+ function statusIcon(status: string): ReactNode {
116
+ switch (status) {
117
+ case "completed":
118
+ return <Check size={ICON_SM} />;
119
+ case "in_progress":
120
+ return <Circle size={ICON_SM} fill="currentColor" />;
121
+ default:
122
+ return <Circle size={ICON_SM} />;
123
+ }
124
+ }
125
+
126
+ /** Renders a TodoWrite tool call as a compact checklist. */
127
+ export function TodoCard({ args }: ToolCardProps): JSX.Element {
128
+ const todos = parseTodos(args);
129
+ const completed: number = todos.filter((t) => t.status === "completed").length;
130
+ const inProgress: TodoItem | undefined = todos.find((t) => t.status === "in_progress");
131
+ const isEmpty: boolean = todos.length === 0;
132
+
133
+ return (
134
+ <div
135
+ className={`${styles.card} ${styles.cardBlue}`}
136
+ data-testid="tool-card-todo"
137
+ >
138
+ <div className={styles.header}>
139
+ <span className={styles.icon}><ListChecks size={ICON_MD} /></span>
140
+ <span className={styles.toolName} style={{ color: "var(--accent-blue)" }}>
141
+ {isEmpty ? "Todos cleared" : "Todos"}
142
+ </span>
143
+ {!isEmpty && (
144
+ <>
145
+ <span className={styles.spacer} />
146
+ <span className={styles.badge} data-testid="tool-card-todo-progress">
147
+ {completed}/{todos.length}
148
+ </span>
149
+ </>
150
+ )}
151
+ </div>
152
+
153
+ {/* Progress bar */}
154
+ {!isEmpty && (
155
+ <div className={todoStyles.progressBar} data-testid="tool-card-todo-bar">
156
+ <div
157
+ className={todoStyles.progressFill}
158
+ style={{ width: `${(completed / todos.length) * 100}%` }}
159
+ />
160
+ </div>
161
+ )}
162
+
163
+ {/* Active task callout */}
164
+ {inProgress && (
165
+ <div className={todoStyles.activeTask} data-testid="tool-card-todo-active">
166
+ <span className={todoStyles.activeIcon}><Circle size={ICON_SM} fill="currentColor" /></span>
167
+ <span className={todoStyles.activeText}>
168
+ {inProgress.activeForm || inProgress.content}
169
+ </span>
170
+ </div>
171
+ )}
172
+
173
+ {/* Checklist */}
174
+ {!isEmpty && (
175
+ <div className={todoStyles.checklist} data-testid="tool-card-todo-list">
176
+ {todos.map((todo, i) => (
177
+ <div
178
+ key={i}
179
+ className={`${todoStyles.item} ${todoStyles[todo.status]}`}
180
+ data-testid="tool-card-todo-item"
181
+ >
182
+ <span className={todoStyles.itemIcon}>
183
+ {statusIcon(todo.status)}
184
+ </span>
185
+ <span className={todoStyles.itemText}>
186
+ {todo.content}
187
+ </span>
188
+ </div>
189
+ ))}
190
+ </div>
191
+ )}
192
+
193
+ {isEmpty && (
194
+ <div className={todoStyles.emptyMessage}>
195
+ All items completed and cleared.
196
+ </div>
197
+ )}
198
+ </div>
199
+ );
200
+ }
@@ -0,0 +1,177 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { expect } from "@storybook/test";
3
+ import { ToolCard } from "./ToolCard.js";
4
+
5
+ const meta: Meta<typeof ToolCard> = {
6
+ component: ToolCard,
7
+ title: "Grackle/Tools/ToolCard",
8
+ tags: ["autodocs"],
9
+ };
10
+ export default meta;
11
+ type Story = StoryObj<typeof meta>;
12
+
13
+ /** File read tool renders the file-read card variant. */
14
+ export const FileRead: Story = {
15
+ args: {
16
+ tool: "Read",
17
+ args: { file_path: "/src/index.ts", limit: 50 },
18
+ result: "import express from 'express';\n\nconst app = express();\napp.listen(3000);",
19
+ },
20
+ play: async ({ canvas }) => {
21
+ await expect(canvas.getByTestId("tool-card-file-read")).toBeInTheDocument();
22
+ await expect(canvas.getByText("index.ts")).toBeInTheDocument();
23
+ },
24
+ };
25
+
26
+ /** Shell command tool renders the shell card variant. */
27
+ export const ShellCommand: Story = {
28
+ args: {
29
+ tool: "Bash",
30
+ args: { command: "npm test" },
31
+ result: "[exit 0] All tests passed.",
32
+ },
33
+ play: async ({ canvas }) => {
34
+ await expect(canvas.getByTestId("tool-card-shell")).toBeInTheDocument();
35
+ await expect(canvas.getByTestId("tool-card-command")).toHaveTextContent("npm test");
36
+ },
37
+ };
38
+
39
+ /** Unknown tool name falls through to the generic card. */
40
+ export const GenericTool: Story = {
41
+ args: {
42
+ tool: "MyCustomTool",
43
+ args: { query: "search term" },
44
+ result: "Found 3 results.",
45
+ },
46
+ play: async ({ canvas }) => {
47
+ await expect(canvas.getByTestId("tool-card-generic")).toBeInTheDocument();
48
+ await expect(canvas.getByText("MyCustomTool")).toBeInTheDocument();
49
+ },
50
+ };
51
+
52
+ /** MCP finding tool (Claude Code format) routes to FindingCard. */
53
+ export const McpFinding: Story = {
54
+ name: "MCP finding_post (Claude Code)",
55
+ args: {
56
+ tool: "mcp__grackle__finding_post",
57
+ args: { title: "Test finding", category: "insight" },
58
+ result: JSON.stringify({ id: "f1", title: "Test finding", category: "insight", tags: [] }),
59
+ },
60
+ play: async ({ canvas }) => {
61
+ await expect(canvas.getByTestId("tool-card-finding")).toBeInTheDocument();
62
+ },
63
+ };
64
+
65
+ /** MCP finding tool (Copilot format) routes to FindingCard. */
66
+ export const McpFindingCopilot: Story = {
67
+ name: "MCP finding_post (Copilot)",
68
+ args: {
69
+ tool: "grackle-finding_post",
70
+ args: { title: "Copilot finding", category: "bug" },
71
+ result: JSON.stringify({ id: "f2", title: "Copilot finding", category: "bug", tags: [] }),
72
+ },
73
+ play: async ({ canvas }) => {
74
+ await expect(canvas.getByTestId("tool-card-finding")).toBeInTheDocument();
75
+ },
76
+ };
77
+
78
+ /** MCP task tool routes to TaskCard. */
79
+ export const McpTask: Story = {
80
+ name: "MCP task_list",
81
+ args: {
82
+ tool: "mcp__grackle__task_list",
83
+ args: {},
84
+ result: JSON.stringify([{ id: "t1", title: "Test", status: "working" }]),
85
+ },
86
+ play: async ({ canvas }) => {
87
+ await expect(canvas.getByTestId("tool-card-task")).toBeInTheDocument();
88
+ },
89
+ };
90
+
91
+ /** MCP workpad tool routes to WorkpadCard. */
92
+ export const McpWorkpad: Story = {
93
+ name: "MCP workpad_write",
94
+ args: {
95
+ tool: "mcp__grackle__workpad_write",
96
+ args: { status: "done", summary: "All done" },
97
+ result: JSON.stringify({ taskId: "t1", workpad: { status: "done", summary: "All done" } }),
98
+ },
99
+ play: async ({ canvas }) => {
100
+ await expect(canvas.getByTestId("tool-card-workpad")).toBeInTheDocument();
101
+ },
102
+ };
103
+
104
+ /** MCP knowledge tool routes to KnowledgeCard. */
105
+ export const McpKnowledge: Story = {
106
+ name: "MCP knowledge_search",
107
+ args: {
108
+ tool: "mcp__grackle__knowledge_search",
109
+ args: { query: "auth" },
110
+ result: JSON.stringify({ results: [], neighbors: [], neighborEdges: [] }),
111
+ },
112
+ play: async ({ canvas }) => {
113
+ await expect(canvas.getByTestId("tool-card-knowledge")).toBeInTheDocument();
114
+ },
115
+ };
116
+
117
+ /** MCP IPC tool routes to IpcCard. */
118
+ export const McpIpc: Story = {
119
+ name: "MCP ipc_spawn",
120
+ args: {
121
+ tool: "mcp__grackle__ipc_spawn",
122
+ args: { prompt: "Run tests", pipe: "detach", environmentId: "local" },
123
+ result: JSON.stringify({ sessionId: "sess-1" }),
124
+ },
125
+ play: async ({ canvas }) => {
126
+ await expect(canvas.getByTestId("tool-card-ipc")).toBeInTheDocument();
127
+ },
128
+ };
129
+
130
+ /** ToolSearch routes to ToolSearchCard. */
131
+ export const ToolSearchRouting: Story = {
132
+ name: "ToolSearch routing",
133
+ args: {
134
+ tool: "ToolSearch",
135
+ args: { query: "select:Read,Write" },
136
+ result: "Read: reads a file\nWrite: writes a file",
137
+ },
138
+ play: async ({ canvas }) => {
139
+ await expect(canvas.getByTestId("tool-card-tool-search")).toBeInTheDocument();
140
+ },
141
+ };
142
+
143
+ /** Claude Code Agent tool routes to the agent card. */
144
+ export const AgentTool: Story = {
145
+ args: {
146
+ tool: "Agent",
147
+ args: { subagent_type: "Explore", description: "Find files", prompt: "Search for files." },
148
+ result: "Found 5 files.",
149
+ },
150
+ play: async ({ canvas }) => {
151
+ await expect(canvas.getByTestId("tool-card-agent")).toBeInTheDocument();
152
+ },
153
+ };
154
+
155
+ /** Copilot task tool routes to the agent card. */
156
+ export const CopilotTaskTool: Story = {
157
+ args: {
158
+ tool: "task",
159
+ args: { agent_type: "explore", name: "search-task", prompt: "Search." },
160
+ result: "Agent started.",
161
+ },
162
+ play: async ({ canvas }) => {
163
+ await expect(canvas.getByTestId("tool-card-agent")).toBeInTheDocument();
164
+ },
165
+ };
166
+
167
+ /** Copilot read_agent tool routes to the agent card. */
168
+ export const CopilotReadAgentTool: Story = {
169
+ args: {
170
+ tool: "read_agent",
171
+ args: { agent_id: "search-task" },
172
+ result: "Agent completed. agent_id: search-task, status: completed, elapsed: 3s",
173
+ },
174
+ play: async ({ canvas }) => {
175
+ await expect(canvas.getByTestId("tool-card-agent")).toBeInTheDocument();
176
+ },
177
+ };
@@ -0,0 +1,60 @@
1
+ import type { JSX } from "react";
2
+ import type { ToolCardProps } from "./ToolCardProps.js";
3
+ import { classifyTool } from "./classifyTool.js";
4
+ import { FileReadCard } from "./FileReadCard.js";
5
+ import { FileEditCard } from "./FileEditCard.js";
6
+ import { ShellCard } from "./ShellCard.js";
7
+ import { SearchCard } from "./SearchCard.js";
8
+ import { TodoCard } from "./TodoCard.js";
9
+ import { MetadataCard } from "./MetadataCard.js";
10
+ import { FindingCard } from "./FindingCard.js";
11
+ import { TaskCard } from "./TaskCard.js";
12
+ import { WorkpadCard } from "./WorkpadCard.js";
13
+ import { KnowledgeCard } from "./KnowledgeCard.js";
14
+ import { IpcCard } from "./IpcCard.js";
15
+ import { ToolSearchCard } from "./ToolSearchCard.js";
16
+ import { GenericToolCard } from "./GenericToolCard.js";
17
+ import { AgentToolCard } from "./AgentToolCard.js";
18
+
19
+ /**
20
+ * Routes a tool event to the appropriate specialized card component.
21
+ *
22
+ * This is a thin classifier + router — all rendering logic lives in the
23
+ * individual card components, which are independently testable via Storybook.
24
+ */
25
+ export function ToolCard(props: ToolCardProps): JSX.Element {
26
+ const category = classifyTool(props.tool);
27
+
28
+ switch (category) {
29
+ case "file-read":
30
+ return <FileReadCard {...props} />;
31
+ case "file-edit":
32
+ return <FileEditCard {...props} />;
33
+ case "file-write":
34
+ return <FileReadCard {...props} writeVariant />;
35
+ case "shell":
36
+ return <ShellCard {...props} />;
37
+ case "search":
38
+ return <SearchCard {...props} />;
39
+ case "todo":
40
+ return <TodoCard {...props} />;
41
+ case "metadata":
42
+ return <MetadataCard {...props} />;
43
+ case "finding":
44
+ return <FindingCard {...props} />;
45
+ case "task":
46
+ return <TaskCard {...props} />;
47
+ case "workpad":
48
+ return <WorkpadCard {...props} />;
49
+ case "knowledge":
50
+ return <KnowledgeCard {...props} />;
51
+ case "ipc":
52
+ return <IpcCard {...props} />;
53
+ case "tool-search":
54
+ return <ToolSearchCard {...props} />;
55
+ case "agent":
56
+ return <AgentToolCard {...props} />;
57
+ default:
58
+ return <GenericToolCard {...props} />;
59
+ }
60
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Shared props interface for all tool card sub-components.
3
+ *
4
+ * The ToolCard router passes these through uniformly to whichever
5
+ * specialized card component handles the tool category.
6
+ */
7
+
8
+ /** Props accepted by every tool card sub-component. */
9
+ export interface ToolCardProps {
10
+ /** Tool name as reported by the runtime (e.g. "Read", "view", "Bash"). */
11
+ tool: string;
12
+ /** Parsed args object from the tool_use event. */
13
+ args: unknown;
14
+ /** Tool result content string (undefined if still in-progress). */
15
+ result?: string;
16
+ /** Whether the tool result is an error. */
17
+ isError?: boolean;
18
+ /** Extended result content (e.g. Copilot's detailedContent with diffs). */
19
+ detailedResult?: string;
20
+ }
@@ -0,0 +1,81 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { expect, userEvent } from "@storybook/test";
3
+ import { ToolSearchCard } from "./ToolSearchCard.js";
4
+
5
+ const meta: Meta<typeof ToolSearchCard> = {
6
+ component: ToolSearchCard,
7
+ title: "Tools/ToolSearchCard",
8
+ };
9
+ export default meta;
10
+ type Story = StoryObj<typeof ToolSearchCard>;
11
+
12
+ export const InProgress: Story = {
13
+ name: "ToolSearch - in progress",
14
+ args: {
15
+ tool: "ToolSearch",
16
+ args: { query: "select:mcp__grackle__finding_post,mcp__grackle__workpad_write", max_results: 3 },
17
+ },
18
+ play: async ({ canvas }) => {
19
+ await expect(canvas.getByTestId("tool-card-tool-search")).toBeInTheDocument();
20
+ await expect(canvas.getByTestId("tool-card-tool-search-query")).toBeInTheDocument();
21
+ },
22
+ };
23
+
24
+ export const WithResults: Story = {
25
+ name: "ToolSearch - with results",
26
+ args: {
27
+ tool: "ToolSearch",
28
+ args: { query: "select:mcp__grackle__finding_post", max_results: 3 },
29
+ result: [
30
+ "mcp__grackle__finding_post:",
31
+ " Post a new finding to the workspace.",
32
+ " Parameters:",
33
+ " title (string, required): Finding title",
34
+ " category (string, optional): Category (bug, insight, decision)",
35
+ " content (string, optional): Detailed content",
36
+ " tags (array, optional): Tags for categorization",
37
+ "",
38
+ "mcp__grackle__workpad_write:",
39
+ " Write to the task workpad.",
40
+ " Parameters:",
41
+ " status (string, optional): Task status",
42
+ " summary (string, optional): Summary text",
43
+ ].join("\n"),
44
+ },
45
+ play: async ({ canvas }) => {
46
+ await expect(canvas.getByTestId("tool-card-tool-search")).toBeInTheDocument();
47
+ await expect(canvas.getByTestId("tool-card-tool-search-result")).toBeInTheDocument();
48
+ await expect(canvas.getByTestId("tool-card-tool-search-count")).toBeInTheDocument();
49
+ },
50
+ };
51
+
52
+ export const LongResultExpandable: Story = {
53
+ name: "ToolSearch - long result with expand",
54
+ args: {
55
+ tool: "ToolSearch",
56
+ args: { query: "grackle tools", max_results: 10 },
57
+ result: Array.from({ length: 20 }, (_, i) => `Line ${i + 1}: tool_${i + 1} definition`).join("\n"),
58
+ },
59
+ play: async ({ canvas }) => {
60
+ await expect(canvas.getByTestId("tool-card-tool-search")).toBeInTheDocument();
61
+ const toggle = canvas.getByTestId("tool-card-toggle");
62
+ await expect(toggle).toBeInTheDocument();
63
+ await expect(toggle).toHaveTextContent("12 more lines");
64
+ await userEvent.click(toggle);
65
+ await expect(toggle).toHaveTextContent("collapse");
66
+ },
67
+ };
68
+
69
+ export const ErrorState: Story = {
70
+ name: "ToolSearch - error",
71
+ args: {
72
+ tool: "ToolSearch",
73
+ args: { query: "nonexistent" },
74
+ result: "No tools found matching query",
75
+ isError: true,
76
+ },
77
+ play: async ({ canvas }) => {
78
+ await expect(canvas.getByTestId("tool-card-tool-search")).toBeInTheDocument();
79
+ await expect(canvas.getByTestId("tool-card-error")).toBeInTheDocument();
80
+ },
81
+ };
@@ -0,0 +1,86 @@
1
+ import { useState, type JSX } from "react";
2
+ import type { ToolCardProps } from "./ToolCardProps.js";
3
+ import styles from "./toolCards.module.scss";
4
+
5
+ /** Extracts query from ToolSearch args. */
6
+ function getQuery(args: unknown): string {
7
+ if (args === null || args === undefined || typeof args !== "object") {
8
+ return "";
9
+ }
10
+ const a = args as Record<string, unknown>;
11
+ return typeof a.query === "string" ? a.query : "";
12
+ }
13
+
14
+ /** Number of lines shown when collapsed. */
15
+ const PREVIEW_LINES: number = 8;
16
+
17
+ /** Renders a ToolSearch call (Claude Code built-in) with query and results. */
18
+ export function ToolSearchCard({ args, result, isError }: ToolCardProps): JSX.Element {
19
+ const [expanded, setExpanded] = useState(false);
20
+ const query = getQuery(args);
21
+ const inProgress = result === undefined;
22
+
23
+ const resultLines = result?.split("\n") ?? [];
24
+ const hasMore = resultLines.length > PREVIEW_LINES;
25
+ const displayResult = expanded ? result : resultLines.slice(0, PREVIEW_LINES).join("\n");
26
+
27
+ return (
28
+ <div
29
+ className={`${styles.card} ${isError ? styles.cardRed : styles.cardNeutral} ${inProgress ? styles.inProgress : ""}`}
30
+ data-testid="tool-card-tool-search"
31
+ >
32
+ <div className={styles.header}>
33
+ <span className={styles.icon} aria-hidden="true">&#x1F527;</span>
34
+ <span className={styles.toolName}>ToolSearch</span>
35
+ {query && (
36
+ <span className={styles.fileName} data-testid="tool-card-tool-search-query">
37
+ &quot;{query}&quot;
38
+ </span>
39
+ )}
40
+ {!inProgress && !isError && result && (
41
+ <>
42
+ <span className={styles.spacer} />
43
+ <span className={styles.badge} data-testid="tool-card-tool-search-count">
44
+ {resultLines.length} lines
45
+ </span>
46
+ </>
47
+ )}
48
+ </div>
49
+
50
+ {/* In-progress: show query */}
51
+ {inProgress && !query && args !== null && args !== undefined && (
52
+ <pre className={styles.pre} data-testid="tool-card-args">
53
+ {JSON.stringify(args, null, 2)}
54
+ </pre>
55
+ )}
56
+
57
+ {/* Error */}
58
+ {isError && result && (
59
+ <pre className={styles.pre} data-testid="tool-card-error">
60
+ {result}
61
+ </pre>
62
+ )}
63
+
64
+ {/* Result text */}
65
+ {!isError && !inProgress && result && (
66
+ <>
67
+ <pre className={styles.pre} data-testid="tool-card-tool-search-result">
68
+ {displayResult}
69
+ </pre>
70
+ {hasMore && (
71
+ <button
72
+ type="button"
73
+ className={styles.bodyToggle}
74
+ onClick={() => { setExpanded((v) => !v); }}
75
+ aria-expanded={expanded}
76
+ data-testid="tool-card-toggle"
77
+ >
78
+ <span className={`${styles.chevron} ${expanded ? styles.chevronExpanded : ""}`}>&#x25B8;</span>
79
+ {expanded ? "collapse" : `${resultLines.length - PREVIEW_LINES} more lines`}
80
+ </button>
81
+ )}
82
+ </>
83
+ )}
84
+ </div>
85
+ );
86
+ }