@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,123 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { expect } from "@storybook/test";
3
+ import { TaskCard } from "./TaskCard.js";
4
+
5
+ const meta: Meta<typeof TaskCard> = {
6
+ component: TaskCard,
7
+ title: "Tools/TaskCard",
8
+ };
9
+ export default meta;
10
+ type Story = StoryObj<typeof TaskCard>;
11
+
12
+ export const CreateInProgress: Story = {
13
+ name: "task_create - in progress",
14
+ args: {
15
+ tool: "mcp__grackle__task_create",
16
+ args: { title: "Fix authentication bug", description: "Session tokens not rotated" },
17
+ },
18
+ play: async ({ canvas }) => {
19
+ await expect(canvas.getByTestId("tool-card-task")).toBeInTheDocument();
20
+ await expect(canvas.getByTestId("tool-card-task-title")).toHaveTextContent("Fix authentication bug");
21
+ },
22
+ };
23
+
24
+ export const CreateCompleted: Story = {
25
+ name: "task_create - completed",
26
+ args: {
27
+ tool: "mcp__grackle__task_create",
28
+ args: { title: "Fix authentication bug" },
29
+ result: JSON.stringify({
30
+ id: "74f5b716",
31
+ title: "Fix authentication bug",
32
+ status: "not_started",
33
+ branch: "default/fix-authentication-bug",
34
+ }),
35
+ },
36
+ play: async ({ canvas }) => {
37
+ await expect(canvas.getByTestId("tool-card-task")).toBeInTheDocument();
38
+ await expect(canvas.getByTestId("tool-card-task-detail")).toBeInTheDocument();
39
+ },
40
+ };
41
+
42
+ export const StartCompleted: Story = {
43
+ name: "task_start - session spawned",
44
+ args: {
45
+ tool: "mcp__grackle__task_start",
46
+ args: { taskId: "74f5b716" },
47
+ result: JSON.stringify({
48
+ sessionId: "1fec3be8-c4f6-423c-a071-9540b2663bc0",
49
+ taskId: "74f5b716",
50
+ }),
51
+ },
52
+ play: async ({ canvas }) => {
53
+ await expect(canvas.getByTestId("tool-card-task")).toBeInTheDocument();
54
+ await expect(canvas.getByTestId("tool-card-task-session")).toHaveTextContent("1fec3be8");
55
+ },
56
+ };
57
+
58
+ export const CompleteStatus: Story = {
59
+ name: "task_complete - marked complete",
60
+ args: {
61
+ tool: "mcp__grackle__task_complete",
62
+ args: { taskId: "74f5b716" },
63
+ result: JSON.stringify({
64
+ id: "74f5b716",
65
+ title: "Fix authentication bug",
66
+ status: "complete",
67
+ branch: "default/fix-authentication-bug",
68
+ }),
69
+ },
70
+ play: async ({ canvas }) => {
71
+ await expect(canvas.getByTestId("tool-card-task")).toBeInTheDocument();
72
+ await expect(canvas.getByTestId("tool-card-task-status")).toBeInTheDocument();
73
+ },
74
+ };
75
+
76
+ export const ListWithTasks: Story = {
77
+ name: "task_list - multiple tasks",
78
+ args: {
79
+ tool: "mcp__grackle__task_list",
80
+ args: {},
81
+ result: JSON.stringify([
82
+ { id: "t1", title: "Fix auth bug", status: "complete" },
83
+ { id: "t2", title: "Add dark mode", status: "working" },
84
+ { id: "t3", title: "Write tests", status: "not_started" },
85
+ { id: "t4", title: "Update docs", status: "paused" },
86
+ { id: "t5", title: "Refactor API", status: "failed" },
87
+ ]),
88
+ },
89
+ play: async ({ canvas }) => {
90
+ await expect(canvas.getByTestId("tool-card-task")).toBeInTheDocument();
91
+ await expect(canvas.getByTestId("tool-card-task-count")).toHaveTextContent("5 tasks");
92
+ await expect(canvas.getByTestId("tool-card-task-list")).toBeInTheDocument();
93
+ },
94
+ };
95
+
96
+ export const CopilotFormat: Story = {
97
+ name: "task_list - Copilot tool name",
98
+ args: {
99
+ tool: "grackle-task_list",
100
+ args: {},
101
+ result: JSON.stringify([
102
+ { id: "t1", title: "Test task", status: "working" },
103
+ ]),
104
+ },
105
+ play: async ({ canvas }) => {
106
+ await expect(canvas.getByTestId("tool-card-task")).toBeInTheDocument();
107
+ await expect(canvas.getByText("task_list")).toBeInTheDocument();
108
+ },
109
+ };
110
+
111
+ export const ErrorState: Story = {
112
+ name: "task_start - error",
113
+ args: {
114
+ tool: "mcp__grackle__task_start",
115
+ args: { taskId: "invalid" },
116
+ result: "gRPC error [NotFound]: task not found",
117
+ isError: true,
118
+ },
119
+ play: async ({ canvas }) => {
120
+ await expect(canvas.getByTestId("tool-card-task")).toBeInTheDocument();
121
+ await expect(canvas.getByTestId("tool-card-error")).toBeInTheDocument();
122
+ },
123
+ };
@@ -0,0 +1,203 @@
1
+ import { useState, type JSX } from "react";
2
+ import type { ToolCardProps } from "./ToolCardProps.js";
3
+ import { extractBareName } from "./classifyTool.js";
4
+ import styles from "./toolCards.module.scss";
5
+
6
+ /** Shape of a task in MCP results. */
7
+ interface TaskSummary {
8
+ id?: string;
9
+ title?: string;
10
+ status?: string;
11
+ branch?: string;
12
+ latestSessionId?: string;
13
+ parentTaskId?: string;
14
+ childTaskIds?: string[];
15
+ }
16
+
17
+ /** Extracts task-relevant fields from tool args. */
18
+ function getArgs(args: unknown): { taskId?: string; title?: string; status?: string } {
19
+ if (args === null || args === undefined || typeof args !== "object") {
20
+ return {};
21
+ }
22
+ const a = args as Record<string, unknown>;
23
+ return {
24
+ taskId: typeof a.taskId === "string" ? a.taskId : undefined,
25
+ title: typeof a.title === "string" ? a.title : undefined,
26
+ status: typeof a.status === "string" ? a.status : undefined,
27
+ };
28
+ }
29
+
30
+ /** Parses MCP result JSON into a task or array of tasks. */
31
+ function parseResult(result: string | undefined): { single?: TaskSummary; list?: TaskSummary[]; sessionId?: string } {
32
+ if (!result) {
33
+ return {};
34
+ }
35
+ try {
36
+ const parsed: unknown = JSON.parse(result);
37
+ if (Array.isArray(parsed)) {
38
+ return { list: (parsed as unknown[]).filter((v): v is TaskSummary => v !== null && typeof v === "object") };
39
+ }
40
+ if (typeof parsed === "object" && parsed !== null) {
41
+ const obj = parsed as Record<string, unknown>;
42
+ // task_start returns { sessionId, taskId }
43
+ if (typeof obj.sessionId === "string") {
44
+ return { sessionId: obj.sessionId, single: obj as TaskSummary };
45
+ }
46
+ return { single: obj as TaskSummary };
47
+ }
48
+ } catch { /* fall through */ }
49
+ return {};
50
+ }
51
+
52
+ /** Status icon for a task. */
53
+ function statusIcon(status: string | undefined): string {
54
+ switch (status) {
55
+ case "complete":
56
+ case "completed":
57
+ return "\u2713";
58
+ case "working":
59
+ case "in_progress":
60
+ return "\u25CF";
61
+ case "paused":
62
+ return "\u23F8";
63
+ case "failed":
64
+ return "\u2717";
65
+ default:
66
+ return "\u25CB";
67
+ }
68
+ }
69
+
70
+ /** CSS color for a task status. */
71
+ function statusColor(status: string | undefined): string {
72
+ switch (status) {
73
+ case "complete":
74
+ case "completed":
75
+ return "var(--accent-green, #4ade80)";
76
+ case "working":
77
+ case "in_progress":
78
+ return "var(--accent-blue, #60a5fa)";
79
+ case "failed":
80
+ return "var(--accent-red, #f87171)";
81
+ case "paused":
82
+ return "var(--text-tertiary, #888)";
83
+ default:
84
+ return "var(--text-secondary, #aaa)";
85
+ }
86
+ }
87
+
88
+ /** Number of tasks shown when collapsed. */
89
+ const PREVIEW_COUNT: number = 5;
90
+
91
+ /** Renders a task tool call with structured display. */
92
+ export function TaskCard({ tool, args, result, isError }: ToolCardProps): JSX.Element {
93
+ const [expanded, setExpanded] = useState(false);
94
+ const bareName = extractBareName(tool);
95
+ const argData = getArgs(args);
96
+ const inProgress = result === undefined;
97
+ const { single, list, sessionId } = parseResult(result);
98
+
99
+ const displayTitle = single?.title ?? argData.title;
100
+ const displayStatus = single?.status ?? argData.status;
101
+
102
+ return (
103
+ <div
104
+ className={`${styles.card} ${isError ? styles.cardRed : styles.cardBlue} ${inProgress ? styles.inProgress : ""}`}
105
+ data-testid="tool-card-task"
106
+ >
107
+ <div className={styles.header}>
108
+ <span className={styles.icon} aria-hidden="true">&#x1F4CB;</span>
109
+ <span className={styles.toolName} style={{ color: "var(--accent-blue)" }}>
110
+ {bareName}
111
+ </span>
112
+ {displayTitle && (
113
+ <span className={styles.fileName} data-testid="tool-card-task-title">
114
+ &quot;{displayTitle}&quot;
115
+ </span>
116
+ )}
117
+ {!displayTitle && argData.taskId && (
118
+ <span className={styles.fileName} data-testid="tool-card-task-id">
119
+ {argData.taskId}
120
+ </span>
121
+ )}
122
+ {displayStatus && (
123
+ <>
124
+ <span className={styles.spacer} />
125
+ <span
126
+ className={styles.badge}
127
+ style={{ color: statusColor(displayStatus) }}
128
+ data-testid="tool-card-task-status"
129
+ >
130
+ {statusIcon(displayStatus)} {displayStatus}
131
+ </span>
132
+ </>
133
+ )}
134
+ {list && !displayStatus && (
135
+ <>
136
+ <span className={styles.spacer} />
137
+ <span className={styles.badge} data-testid="tool-card-task-count">
138
+ {list.length} {list.length === 1 ? "task" : "tasks"}
139
+ </span>
140
+ </>
141
+ )}
142
+ </div>
143
+
144
+ {/* In-progress: show args */}
145
+ {inProgress && !displayTitle && !argData.taskId && args !== null && args !== undefined && (
146
+ <pre className={styles.pre} data-testid="tool-card-args">
147
+ {JSON.stringify(args, null, 2)}
148
+ </pre>
149
+ )}
150
+
151
+ {/* Error */}
152
+ {isError && result && (
153
+ <pre className={styles.pre} data-testid="tool-card-error">
154
+ {result}
155
+ </pre>
156
+ )}
157
+
158
+ {/* Session ID from task_start */}
159
+ {!isError && sessionId && (
160
+ <div className={styles.pre} style={{ padding: "4px 8px", fontSize: "0.85em" }} data-testid="tool-card-task-session">
161
+ session: {sessionId}
162
+ </div>
163
+ )}
164
+
165
+ {/* Single task: show key fields */}
166
+ {!isError && single && !list && !sessionId && (
167
+ <pre className={styles.pre} data-testid="tool-card-task-detail">
168
+ {[
169
+ single.id ? `id: ${single.id}` : null,
170
+ single.status ? `status: ${single.status}` : null,
171
+ single.branch ? `branch: ${single.branch}` : null,
172
+ single.latestSessionId ? `session: ${single.latestSessionId}` : null,
173
+ ].filter(Boolean).join("\n")}
174
+ </pre>
175
+ )}
176
+
177
+ {/* Task list */}
178
+ {!isError && list && list.length > 0 && (
179
+ <>
180
+ <pre className={styles.pre} data-testid="tool-card-task-list">
181
+ {(expanded ? list : list.slice(0, PREVIEW_COUNT)).map((t) => {
182
+ const icon = statusIcon(t.status);
183
+ const title = t.title ?? t.id ?? "untitled";
184
+ return `${icon} ${title}`;
185
+ }).join("\n")}
186
+ </pre>
187
+ {list.length > PREVIEW_COUNT && (
188
+ <button
189
+ type="button"
190
+ className={styles.bodyToggle}
191
+ onClick={() => { setExpanded((v) => !v); }}
192
+ aria-expanded={expanded}
193
+ data-testid="tool-card-toggle"
194
+ >
195
+ <span className={`${styles.chevron} ${expanded ? styles.chevronExpanded : ""}`}>&#x25B8;</span>
196
+ {expanded ? "collapse" : `${list.length - PREVIEW_COUNT} more tasks`}
197
+ </button>
198
+ )}
199
+ </>
200
+ )}
201
+ </div>
202
+ );
203
+ }
@@ -0,0 +1,131 @@
1
+ @use '../../styles/mixins' as *;
2
+
3
+ // =============================================================================
4
+ // TodoCard — checklist with progress bar and active task highlight
5
+ // =============================================================================
6
+
7
+ // --- Progress bar ---
8
+
9
+ .progressBar {
10
+ height: 3px;
11
+ background: var(--bg-inset);
12
+ border-radius: 2px;
13
+ margin: var(--space-xs) 0;
14
+ overflow: hidden;
15
+ }
16
+
17
+ .progressFill {
18
+ height: 100%;
19
+ background: var(--accent-green);
20
+ border-radius: 2px;
21
+ transition: width 0.3s ease;
22
+ }
23
+
24
+ // --- Active task callout ---
25
+
26
+ .activeTask {
27
+ display: flex;
28
+ align-items: center;
29
+ gap: var(--space-xs);
30
+ padding: var(--space-xs) var(--space-sm);
31
+ margin: var(--space-xs) 0;
32
+ background: rgba(59, 130, 246, 0.08);
33
+ border-radius: var(--radius-sm);
34
+ font-size: var(--font-size-sm);
35
+ }
36
+
37
+ .activeIcon {
38
+ color: var(--accent-blue);
39
+ font-size: 8px;
40
+ animation: todoPulse 1.5s ease-in-out infinite;
41
+ flex-shrink: 0;
42
+ }
43
+
44
+ .activeText {
45
+ color: var(--accent-blue);
46
+ font-weight: var(--font-weight-medium);
47
+ font-style: italic;
48
+ }
49
+
50
+ @keyframes todoPulse {
51
+ 0%, 100% { opacity: 0.4; }
52
+ 50% { opacity: 1; }
53
+ }
54
+
55
+ // --- Checklist ---
56
+
57
+ .checklist {
58
+ display: flex;
59
+ flex-direction: column;
60
+ gap: 1px;
61
+ margin-top: var(--space-xs);
62
+ }
63
+
64
+ .item {
65
+ display: flex;
66
+ align-items: center;
67
+ gap: var(--space-sm);
68
+ padding: 3px var(--space-xs);
69
+ font-size: var(--font-size-sm);
70
+ border-radius: var(--radius-sm);
71
+ transition: background var(--transition-fast);
72
+ }
73
+
74
+ .itemIcon {
75
+ flex-shrink: 0;
76
+ width: 14px;
77
+ text-align: center;
78
+ font-size: 11px;
79
+ font-weight: var(--font-weight-bold);
80
+ }
81
+
82
+ .itemText {
83
+ flex: 1;
84
+ min-width: 0;
85
+ }
86
+
87
+ // --- Status variants ---
88
+
89
+ .pending {
90
+ color: var(--text-tertiary);
91
+
92
+ .itemIcon {
93
+ color: var(--text-tertiary);
94
+ }
95
+ }
96
+
97
+ .in_progress {
98
+ color: var(--text-primary);
99
+ background: rgba(59, 130, 246, 0.05);
100
+
101
+ .itemIcon {
102
+ color: var(--accent-blue);
103
+ animation: todoPulse 1.5s ease-in-out infinite;
104
+ }
105
+
106
+ .itemText {
107
+ font-weight: var(--font-weight-medium);
108
+ }
109
+ }
110
+
111
+ .completed {
112
+ color: var(--text-secondary);
113
+
114
+ .itemIcon {
115
+ color: var(--accent-green);
116
+ }
117
+
118
+ .itemText {
119
+ text-decoration: line-through;
120
+ text-decoration-color: var(--text-tertiary);
121
+ }
122
+ }
123
+
124
+ // --- Empty state ---
125
+
126
+ .emptyMessage {
127
+ font-size: var(--font-size-sm);
128
+ color: var(--text-tertiary);
129
+ font-style: italic;
130
+ padding: var(--space-xs) 0;
131
+ }
@@ -0,0 +1,202 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { expect } from "@storybook/test";
3
+ import { TodoCard } from "./TodoCard.js";
4
+
5
+ const meta: Meta<typeof TodoCard> = {
6
+ component: TodoCard,
7
+ title: "Grackle/Tools/TodoCard",
8
+ tags: ["autodocs"],
9
+ };
10
+ export default meta;
11
+ type Story = StoryObj<typeof meta>;
12
+
13
+ export const AllPending: Story = {
14
+ name: "Initial - all pending",
15
+ args: {
16
+ tool: "TodoWrite",
17
+ args: {
18
+ todos: [
19
+ { content: "Get bread", activeForm: "Getting bread", status: "pending" },
20
+ { content: "Spread peanut butter", activeForm: "Spreading peanut butter", status: "pending" },
21
+ { content: "Spread jelly", activeForm: "Spreading jelly", status: "pending" },
22
+ { content: "Combine slices", activeForm: "Combining slices", status: "pending" },
23
+ { content: "Cut in half", activeForm: "Cutting in half", status: "pending" },
24
+ ],
25
+ },
26
+ },
27
+ play: async ({ canvas }) => {
28
+ await expect(canvas.getByTestId("tool-card-todo")).toBeInTheDocument();
29
+ await expect(canvas.getByTestId("tool-card-todo-progress")).toHaveTextContent("0/5");
30
+ await expect(canvas.getAllByTestId("tool-card-todo-item")).toHaveLength(5);
31
+ // No active task when all pending
32
+ await expect(canvas.queryByTestId("tool-card-todo-active")).not.toBeInTheDocument();
33
+ },
34
+ };
35
+
36
+ export const FirstInProgress: Story = {
37
+ name: "Step 1 in progress",
38
+ args: {
39
+ tool: "TodoWrite",
40
+ args: {
41
+ todos: [
42
+ { content: "Get bread", activeForm: "Getting bread", status: "in_progress" },
43
+ { content: "Spread peanut butter", activeForm: "Spreading peanut butter", status: "pending" },
44
+ { content: "Spread jelly", activeForm: "Spreading jelly", status: "pending" },
45
+ { content: "Combine slices", activeForm: "Combining slices", status: "pending" },
46
+ { content: "Cut in half", activeForm: "Cutting in half", status: "pending" },
47
+ ],
48
+ },
49
+ },
50
+ play: async ({ canvas }) => {
51
+ await expect(canvas.getByTestId("tool-card-todo-progress")).toHaveTextContent("0/5");
52
+ // Active task callout should show the activeForm text
53
+ const active = canvas.getByTestId("tool-card-todo-active");
54
+ await expect(active).toBeInTheDocument();
55
+ await expect(active).toHaveTextContent("Getting bread");
56
+ },
57
+ };
58
+
59
+ export const MidwayThrough: Story = {
60
+ name: "Midway - 2 done, 1 in progress",
61
+ args: {
62
+ tool: "TodoWrite",
63
+ args: {
64
+ todos: [
65
+ { content: "Get bread", activeForm: "Getting bread", status: "completed" },
66
+ { content: "Spread peanut butter", activeForm: "Spreading peanut butter", status: "completed" },
67
+ { content: "Spread jelly", activeForm: "Spreading jelly", status: "in_progress" },
68
+ { content: "Combine slices", activeForm: "Combining slices", status: "pending" },
69
+ { content: "Cut in half", activeForm: "Cutting in half", status: "pending" },
70
+ ],
71
+ },
72
+ },
73
+ play: async ({ canvas }) => {
74
+ await expect(canvas.getByTestId("tool-card-todo-progress")).toHaveTextContent("2/5");
75
+ await expect(canvas.getByTestId("tool-card-todo-active")).toHaveTextContent("Spreading jelly");
76
+ },
77
+ };
78
+
79
+ export const AllCompleted: Story = {
80
+ name: "All completed",
81
+ args: {
82
+ tool: "TodoWrite",
83
+ args: {
84
+ todos: [
85
+ { content: "Get bread", activeForm: "Getting bread", status: "completed" },
86
+ { content: "Spread peanut butter", activeForm: "Spreading peanut butter", status: "completed" },
87
+ { content: "Spread jelly", activeForm: "Spreading jelly", status: "completed" },
88
+ { content: "Combine slices", activeForm: "Combining slices", status: "completed" },
89
+ { content: "Cut in half", activeForm: "Cutting in half", status: "completed" },
90
+ ],
91
+ },
92
+ },
93
+ play: async ({ canvas }) => {
94
+ await expect(canvas.getByTestId("tool-card-todo-progress")).toHaveTextContent("5/5");
95
+ // No active task when all done
96
+ await expect(canvas.queryByTestId("tool-card-todo-active")).not.toBeInTheDocument();
97
+ },
98
+ };
99
+
100
+ export const Cleared: Story = {
101
+ name: "Cleared - empty list",
102
+ args: {
103
+ tool: "TodoWrite",
104
+ args: { todos: [] },
105
+ },
106
+ play: async ({ canvas }) => {
107
+ await expect(canvas.getByTestId("tool-card-todo")).toBeInTheDocument();
108
+ await expect(canvas.queryByTestId("tool-card-todo-list")).not.toBeInTheDocument();
109
+ await expect(canvas.queryByTestId("tool-card-todo-progress")).not.toBeInTheDocument();
110
+ },
111
+ };
112
+
113
+ export const NearEnd: Story = {
114
+ name: "Near end - 4 done, 1 in progress",
115
+ args: {
116
+ tool: "TodoWrite",
117
+ args: {
118
+ todos: [
119
+ { content: "Get bread", activeForm: "Getting bread", status: "completed" },
120
+ { content: "Spread peanut butter", activeForm: "Spreading peanut butter", status: "completed" },
121
+ { content: "Spread jelly", activeForm: "Spreading jelly", status: "completed" },
122
+ { content: "Combine slices", activeForm: "Combining slices", status: "completed" },
123
+ { content: "Cut in half", activeForm: "Cutting in half", status: "in_progress" },
124
+ ],
125
+ },
126
+ },
127
+ play: async ({ canvas }) => {
128
+ await expect(canvas.getByTestId("tool-card-todo-progress")).toHaveTextContent("4/5");
129
+ await expect(canvas.getByTestId("tool-card-todo-active")).toHaveTextContent("Cutting in half");
130
+ },
131
+ };
132
+
133
+ export const NoActiveForm: Story = {
134
+ name: "No activeForm - falls back to content",
135
+ args: {
136
+ tool: "TodoWrite",
137
+ args: {
138
+ todos: [
139
+ { content: "Research API options", status: "in_progress" },
140
+ { content: "Write implementation", status: "pending" },
141
+ { content: "Add tests", status: "pending" },
142
+ ],
143
+ },
144
+ },
145
+ play: async ({ canvas }) => {
146
+ await expect(canvas.getByTestId("tool-card-todo-active")).toHaveTextContent("Research API options");
147
+ },
148
+ };
149
+
150
+ // --- Codex update_plan format ---
151
+
152
+ export const CodexPlan: Story = {
153
+ name: "Codex - update_plan format",
154
+ args: {
155
+ tool: "update_plan",
156
+ args: {
157
+ explanation: "Working through the sandwich steps",
158
+ plan: [
159
+ { step: "Get bread from pantry", status: "completed" },
160
+ { step: "Spread peanut butter on slice", status: "in_progress" },
161
+ { step: "Spread jelly on other slice", status: "pending" },
162
+ { step: "Combine slices together", status: "pending" },
163
+ ],
164
+ },
165
+ },
166
+ play: async ({ canvas }) => {
167
+ await expect(canvas.getByTestId("tool-card-todo-progress")).toHaveTextContent("1/4");
168
+ await expect(canvas.getByTestId("tool-card-todo-active")).toHaveTextContent("Spread peanut butter on slice");
169
+ await expect(canvas.getAllByTestId("tool-card-todo-item")).toHaveLength(4);
170
+ },
171
+ };
172
+
173
+ // --- Goose todo_write format (markdown checklist) ---
174
+
175
+ export const GooseMarkdown: Story = {
176
+ name: "Goose - markdown checklist",
177
+ args: {
178
+ tool: "todo_write",
179
+ args: {
180
+ content: "- [x] Get two slices of bread\n- [x] Open peanut butter jar\n- [~] Spread PB on first slice\n- [ ] Spread jelly on second slice\n- [ ] Press slices together\n- [ ] Cut diagonally",
181
+ },
182
+ },
183
+ play: async ({ canvas }) => {
184
+ await expect(canvas.getByTestId("tool-card-todo-progress")).toHaveTextContent("2/6");
185
+ await expect(canvas.getByTestId("tool-card-todo-active")).toHaveTextContent("Spread PB on first slice");
186
+ await expect(canvas.getAllByTestId("tool-card-todo-item")).toHaveLength(6);
187
+ },
188
+ };
189
+
190
+ export const GooseAllChecked: Story = {
191
+ name: "Goose - all checked off",
192
+ args: {
193
+ tool: "todo_write",
194
+ args: {
195
+ content: "- [x] Get bread\n- [x] Spread PB\n- [x] Spread jelly\n- [x] Combine\n- [x] Cut",
196
+ },
197
+ },
198
+ play: async ({ canvas }) => {
199
+ await expect(canvas.getByTestId("tool-card-todo-progress")).toHaveTextContent("5/5");
200
+ await expect(canvas.queryByTestId("tool-card-todo-active")).not.toBeInTheDocument();
201
+ },
202
+ };