@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,132 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { expect, fn } from "@storybook/test";
3
+ import type { StreamData } from "../../hooks/types.js";
4
+ import { withMockGrackleRoute } from "../../test-utils/storybook-helpers.js";
5
+ import { StreamDetailPanel } from "./StreamDetailPanel.js";
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Mock data
9
+ // ---------------------------------------------------------------------------
10
+
11
+ const streamWithSubscribers: StreamData = {
12
+ id: "stream-abc123",
13
+ name: "agent-chat",
14
+ subscriberCount: 2,
15
+ messageBufferDepth: 5,
16
+ subscribers: [
17
+ {
18
+ subscriptionId: "sub-001",
19
+ sessionId: "session-aabbccdd-eeff-0011",
20
+ fd: 3,
21
+ permission: "rw",
22
+ deliveryMode: "async",
23
+ createdBySpawn: true,
24
+ },
25
+ {
26
+ subscriptionId: "sub-002",
27
+ sessionId: "session-11223344-5566-7788",
28
+ fd: 4,
29
+ permission: "r",
30
+ deliveryMode: "sync",
31
+ createdBySpawn: false,
32
+ },
33
+ ],
34
+ };
35
+
36
+ const streamNoSubscribers: StreamData = {
37
+ id: "stream-empty",
38
+ name: "telemetry-feed",
39
+ subscriberCount: 0,
40
+ messageBufferDepth: 0,
41
+ subscribers: [],
42
+ };
43
+
44
+ const streamAllModes: StreamData = {
45
+ id: "stream-modes",
46
+ name: "mixed-modes",
47
+ subscriberCount: 3,
48
+ messageBufferDepth: 0,
49
+ subscribers: [
50
+ {
51
+ subscriptionId: "sub-rw-async",
52
+ sessionId: "session-rw-async",
53
+ fd: 3,
54
+ permission: "rw",
55
+ deliveryMode: "async",
56
+ createdBySpawn: true,
57
+ },
58
+ {
59
+ subscriptionId: "sub-r-sync",
60
+ sessionId: "session-r-sync",
61
+ fd: 4,
62
+ permission: "r",
63
+ deliveryMode: "sync",
64
+ createdBySpawn: false,
65
+ },
66
+ {
67
+ subscriptionId: "sub-w-detach",
68
+ sessionId: "session-w-detach",
69
+ fd: 5,
70
+ permission: "w",
71
+ deliveryMode: "detach",
72
+ createdBySpawn: false,
73
+ },
74
+ ],
75
+ };
76
+
77
+ // ---------------------------------------------------------------------------
78
+ // Story meta
79
+ // ---------------------------------------------------------------------------
80
+
81
+ const meta: Meta<typeof StreamDetailPanel> = {
82
+ title: "Grackle/Streams/StreamDetailPanel",
83
+ component: StreamDetailPanel,
84
+ decorators: [withMockGrackleRoute(["/chat/stream-abc123"], "/chat/:streamId")],
85
+ parameters: { skipRouter: true },
86
+ args: {
87
+ stream: streamWithSubscribers,
88
+ onClose: fn(),
89
+ },
90
+ };
91
+
92
+ export default meta;
93
+ type Story = StoryObj<typeof StreamDetailPanel>;
94
+
95
+ // ---------------------------------------------------------------------------
96
+ // Stories
97
+ // ---------------------------------------------------------------------------
98
+
99
+ /** Stream with multiple active subscribers. */
100
+ export const WithSubscribers: Story = {
101
+ play: async ({ canvas }) => {
102
+ await expect(canvas.getByTestId("stream-detail-panel")).toBeInTheDocument();
103
+ await expect(canvas.getByTestId("subscriber-card-sub-001")).toBeInTheDocument();
104
+ await expect(canvas.getByTestId("subscriber-card-sub-002")).toBeInTheDocument();
105
+ },
106
+ };
107
+
108
+ /** Stream with no subscribers. */
109
+ export const NoSubscribers: Story = {
110
+ args: {
111
+ stream: streamNoSubscribers,
112
+ },
113
+ play: async ({ canvas }) => {
114
+ await expect(canvas.getByText("No active subscribers")).toBeInTheDocument();
115
+ },
116
+ };
117
+
118
+ /** All permission and delivery mode badge variants. */
119
+ export const AllPermissionModes: Story = {
120
+ args: {
121
+ stream: streamAllModes,
122
+ },
123
+ };
124
+
125
+ /** Close button calls onClose. */
126
+ export const CloseButton: Story = {
127
+ play: async ({ canvas, args }) => {
128
+ const closeBtn = canvas.getByRole("button", { name: /close stream details/i });
129
+ closeBtn.click();
130
+ await expect(args.onClose).toHaveBeenCalled();
131
+ },
132
+ };
@@ -0,0 +1,119 @@
1
+ /**
2
+ * StreamDetailPanel — right pull-out drawer showing stream metadata.
3
+ *
4
+ * Renders as an absolutely-positioned overlay anchored to the right of its
5
+ * containing block (which must have `position: relative`).
6
+ *
7
+ * @module
8
+ */
9
+
10
+ import { useEffect, type JSX } from "react";
11
+ import type { StreamData } from "../../hooks/types.js";
12
+ import { useAppNavigate, sessionUrl } from "../../utils/navigation.js";
13
+ import styles from "./StreamDetailPanel.module.scss";
14
+
15
+ /** Props for the StreamDetailPanel component. */
16
+ export interface StreamDetailPanelProps {
17
+ /** The stream to display details for. */
18
+ stream: StreamData;
19
+ /** Called when the user requests to close the panel. */
20
+ onClose: () => void;
21
+ }
22
+
23
+ /** Render a permission badge with appropriate color. */
24
+ function PermissionBadge({ permission }: { permission: string }): JSX.Element {
25
+ const cls = permission === "rw"
26
+ ? styles.badgeRw
27
+ : permission === "r"
28
+ ? styles.badgeR
29
+ : styles.badgeW;
30
+ return <span className={cls}>{permission}</span>;
31
+ }
32
+
33
+ /** Render a delivery mode badge with appropriate color. */
34
+ function DeliveryModeBadge({ mode }: { mode: string }): JSX.Element {
35
+ const cls = mode === "async"
36
+ ? styles.badgeAsync
37
+ : mode === "detach"
38
+ ? styles.badgeDetach
39
+ : styles.badgeSync;
40
+ return <span className={cls}>{mode}</span>;
41
+ }
42
+
43
+ /**
44
+ * Pull-out right drawer showing stream metadata: overview, subscribers, fds.
45
+ */
46
+ export function StreamDetailPanel({ stream, onClose }: StreamDetailPanelProps): JSX.Element {
47
+ const navigate = useAppNavigate();
48
+
49
+ // Close on Escape key
50
+ useEffect(() => {
51
+ const handleKeyDown = (e: KeyboardEvent): void => {
52
+ if (e.key === "Escape") {
53
+ onClose();
54
+ }
55
+ };
56
+ document.addEventListener("keydown", handleKeyDown);
57
+ return () => { document.removeEventListener("keydown", handleKeyDown); };
58
+ }, [onClose]);
59
+
60
+ return (
61
+ <div className={styles.panel} data-testid="stream-detail-panel">
62
+ <div className={styles.header}>
63
+ <h3 className={styles.title}>{stream.name}</h3>
64
+ <button className={styles.closeButton} onClick={onClose} aria-label="Close stream details">
65
+ &times;
66
+ </button>
67
+ </div>
68
+
69
+ <div className={styles.body}>
70
+ {/* Overview */}
71
+ <div className={styles.section}>
72
+ <div className={styles.sectionLabel}>Overview</div>
73
+ <div className={styles.metaRow}>
74
+ <span className={styles.metaKey}>Stream ID</span>
75
+ <span className={styles.metaValue}>{stream.id}</span>
76
+ </div>
77
+ <div className={styles.metaRow}>
78
+ <span className={styles.metaKey}>Subscribers</span>
79
+ <span className={styles.metaValue}>{stream.subscriberCount}</span>
80
+ </div>
81
+ <div className={styles.metaRow}>
82
+ <span className={styles.metaKey}>Buffered</span>
83
+ <span className={styles.metaValue}>{stream.messageBufferDepth} msgs</span>
84
+ </div>
85
+ </div>
86
+
87
+ {/* Subscribers */}
88
+ <div className={styles.section}>
89
+ <div className={styles.sectionLabel}>Subscribers</div>
90
+ {stream.subscribers.length === 0 ? (
91
+ <div className={styles.emptySubscribers}>No active subscribers</div>
92
+ ) : (
93
+ stream.subscribers.map((sub) => (
94
+ <div key={sub.subscriptionId} className={styles.subscriberCard} data-testid={`subscriber-card-${sub.subscriptionId}`}>
95
+ <div className={styles.subscriberHeader}>
96
+ <span className={styles.fdNumber}>fd {sub.fd}</span>
97
+ <button
98
+ className={styles.sessionLink}
99
+ onClick={() => { navigate(sessionUrl(sub.sessionId)); }}
100
+ title={sub.sessionId}
101
+ >
102
+ {sub.sessionId.slice(0, 12)}…
103
+ </button>
104
+ </div>
105
+ <div className={styles.badges}>
106
+ <PermissionBadge permission={sub.permission} />
107
+ <DeliveryModeBadge mode={sub.deliveryMode} />
108
+ {sub.createdBySpawn && (
109
+ <span className={styles.spawnTag}>spawn</span>
110
+ )}
111
+ </div>
112
+ </div>
113
+ ))
114
+ )}
115
+ </div>
116
+ </div>
117
+ </div>
118
+ );
119
+ }
@@ -0,0 +1,92 @@
1
+ @use '../../styles/mixins' as *;
2
+
3
+ // =============================================================================
4
+ // StreamList — IPC stream sidebar list
5
+ // =============================================================================
6
+
7
+ .container {
8
+ padding: var(--space-sm) 0;
9
+ }
10
+
11
+ .header {
12
+ @include section-label;
13
+ padding: var(--space-xs) var(--space-md);
14
+ display: flex;
15
+ align-items: center;
16
+ justify-content: space-between;
17
+ }
18
+
19
+ .refreshButton {
20
+ background: none;
21
+ border: none;
22
+ color: var(--text-secondary);
23
+ cursor: pointer;
24
+ padding: 2px;
25
+ display: flex;
26
+ align-items: center;
27
+ border-radius: var(--radius-sm);
28
+
29
+ &:hover {
30
+ color: var(--text-primary);
31
+ background: var(--bg-overlay);
32
+ }
33
+ }
34
+
35
+ .streamRow {
36
+ @include hover-accent;
37
+ display: flex;
38
+ align-items: center;
39
+ gap: var(--space-sm);
40
+ padding: var(--space-xs) var(--space-md);
41
+ cursor: pointer;
42
+ min-height: 32px;
43
+ user-select: none;
44
+
45
+ &.selected {
46
+ background: var(--bg-overlay);
47
+ }
48
+ }
49
+
50
+ .systemRow {
51
+ composes: streamRow;
52
+ border-bottom: 1px solid var(--border-subtle);
53
+ margin-bottom: var(--space-xs);
54
+ font-weight: 500;
55
+ }
56
+
57
+ .streamIcon {
58
+ flex-shrink: 0;
59
+ color: var(--text-secondary);
60
+ }
61
+
62
+ .streamName {
63
+ flex: 1;
64
+ font-size: 13px;
65
+ color: var(--text-primary);
66
+ overflow: hidden;
67
+ text-overflow: ellipsis;
68
+ white-space: nowrap;
69
+ }
70
+
71
+ .subscriberBadge {
72
+ @include surface-inset;
73
+ font-size: 11px;
74
+ padding: 1px 6px;
75
+ border-radius: var(--radius-sm);
76
+ color: var(--text-secondary);
77
+ flex-shrink: 0;
78
+ }
79
+
80
+ .emptyState {
81
+ padding: var(--space-md);
82
+ font-size: 12px;
83
+ color: var(--text-disabled);
84
+ text-align: center;
85
+ }
86
+
87
+ .loading {
88
+ padding: var(--space-md);
89
+ font-size: 12px;
90
+ color: var(--text-disabled);
91
+ text-align: center;
92
+ }
@@ -0,0 +1,99 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { expect, fn } from "@storybook/test";
3
+ import type { StreamData } from "../../hooks/types.js";
4
+ import { withMockGrackleRoute } from "../../test-utils/storybook-helpers.js";
5
+ import { StreamList } from "./StreamList.js";
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Mock data
9
+ // ---------------------------------------------------------------------------
10
+
11
+ const mockStreams: StreamData[] = [
12
+ {
13
+ id: "stream-001",
14
+ name: "agent-chat",
15
+ subscriberCount: 2,
16
+ messageBufferDepth: 0,
17
+ subscribers: [],
18
+ },
19
+ {
20
+ id: "stream-002",
21
+ name: "coordinator-bus",
22
+ subscriberCount: 1,
23
+ messageBufferDepth: 3,
24
+ subscribers: [],
25
+ },
26
+ {
27
+ id: "stream-003",
28
+ name: "telemetry-feed",
29
+ subscriberCount: 0,
30
+ messageBufferDepth: 0,
31
+ subscribers: [],
32
+ },
33
+ ];
34
+
35
+ // ---------------------------------------------------------------------------
36
+ // Story meta
37
+ // ---------------------------------------------------------------------------
38
+
39
+ const meta: Meta<typeof StreamList> = {
40
+ title: "Grackle/Streams/StreamList",
41
+ component: StreamList,
42
+ parameters: { skipRouter: true },
43
+ args: {
44
+ streams: mockStreams,
45
+ loading: false,
46
+ onRefresh: fn(),
47
+ },
48
+ };
49
+
50
+ export default meta;
51
+ type Story = StoryObj<typeof StreamList>;
52
+
53
+ // ---------------------------------------------------------------------------
54
+ // Stories
55
+ // ---------------------------------------------------------------------------
56
+
57
+ /** Default: shows System pinned row and a list of named streams. */
58
+ export const Default: Story = {
59
+ decorators: [withMockGrackleRoute(["/chat"], "/chat")],
60
+ };
61
+
62
+ /** Empty state: no named streams, only the System row. */
63
+ export const Empty: Story = {
64
+ decorators: [withMockGrackleRoute(["/chat"], "/chat")],
65
+ args: {
66
+ streams: [],
67
+ },
68
+ };
69
+
70
+ /** Loading state while streams are being fetched. */
71
+ export const Loading: Story = {
72
+ decorators: [withMockGrackleRoute(["/chat"], "/chat")],
73
+ args: {
74
+ streams: [],
75
+ loading: true,
76
+ },
77
+ };
78
+
79
+ /** System row is visually selected when on the /chat route. */
80
+ export const SystemSelected: Story = {
81
+ decorators: [withMockGrackleRoute(["/chat"], "/chat")],
82
+ play: async ({ canvas }) => {
83
+ const systemRow = canvas.getByTestId("stream-list-system-row");
84
+ await expect(systemRow).toBeInTheDocument();
85
+ await expect(systemRow).toHaveAttribute("aria-current", "page");
86
+ },
87
+ };
88
+
89
+ /** A named stream is selected when on its /chat/:streamId route. */
90
+ export const StreamSelected: Story = {
91
+ decorators: [withMockGrackleRoute(["/chat/stream-001"], "/chat/:streamId")],
92
+ play: async ({ canvas }) => {
93
+ const streamRow = canvas.getByTestId("stream-list-row-stream-001");
94
+ await expect(streamRow).toBeInTheDocument();
95
+ await expect(streamRow).toHaveAttribute("aria-current", "page");
96
+ const systemRow = canvas.getByTestId("stream-list-system-row");
97
+ await expect(systemRow).not.toHaveAttribute("aria-current");
98
+ },
99
+ };
@@ -0,0 +1,114 @@
1
+ /**
2
+ * StreamList — sidebar list of IPC streams with a pinned "System" entry.
3
+ *
4
+ * @module
5
+ */
6
+
7
+ import { useCallback, type JSX } from "react";
8
+ import { useLocation, useMatch } from "react-router";
9
+ import { MessageSquare, Radio, RefreshCw } from "lucide-react";
10
+ import type { StreamData } from "../../hooks/types.js";
11
+ import { useAppNavigate, chatStreamUrl, CHAT_URL } from "../../utils/navigation.js";
12
+ import styles from "./StreamList.module.scss";
13
+
14
+ /** Size for row icons. */
15
+ const ICON_SIZE: number = 14;
16
+
17
+ /** Props for the StreamList sidebar component. */
18
+ export interface StreamListProps {
19
+ /** All known IPC streams. */
20
+ streams: StreamData[];
21
+ /** Whether streams are currently loading. */
22
+ loading: boolean;
23
+ /** True if the most recent load attempt failed. */
24
+ streamsLoadError?: boolean;
25
+ /** True after at least one load attempt has completed. */
26
+ streamsLoadedOnce?: boolean;
27
+ /** Optional callback to trigger a stream list refresh. */
28
+ onRefresh?: () => void;
29
+ }
30
+
31
+ /**
32
+ * Sidebar list showing IPC streams.
33
+ *
34
+ * The "System" row is always pinned at the top and links to `/chat`.
35
+ * Named streams are listed below, sorted alphabetically.
36
+ */
37
+ export function StreamList({ streams, loading, streamsLoadError = false, streamsLoadedOnce = true, onRefresh }: StreamListProps): JSX.Element {
38
+ const navigate = useAppNavigate();
39
+ const location = useLocation();
40
+ const streamMatch = useMatch("/chat/:streamId");
41
+
42
+ const selectedStreamId = streamMatch?.params.streamId;
43
+ const isSystemSelected = !selectedStreamId && location.pathname === CHAT_URL;
44
+
45
+ const sortedStreams = [...streams].sort((a, b) => a.name.localeCompare(b.name));
46
+
47
+ const handleSystemClick = useCallback(() => {
48
+ navigate(CHAT_URL);
49
+ }, [navigate]);
50
+
51
+ const handleStreamClick = useCallback((streamId: string) => {
52
+ navigate(chatStreamUrl(streamId));
53
+ }, [navigate]);
54
+
55
+ return (
56
+ <div className={styles.container} data-testid="stream-list">
57
+ <div className={styles.header}>
58
+ <span>Streams</span>
59
+ {onRefresh && (
60
+ <button
61
+ className={styles.refreshButton}
62
+ onClick={onRefresh}
63
+ aria-label="Refresh streams"
64
+ data-testid="stream-list-refresh"
65
+ >
66
+ <RefreshCw size={12} />
67
+ </button>
68
+ )}
69
+ </div>
70
+
71
+ {/* Pinned System row */}
72
+ <button
73
+ type="button"
74
+ className={`${styles.systemRow}${isSystemSelected ? ` ${styles.selected}` : ""}`}
75
+ onClick={handleSystemClick}
76
+ data-testid="stream-list-system-row"
77
+ aria-current={isSystemSelected ? "page" : undefined}
78
+ >
79
+ <MessageSquare size={ICON_SIZE} className={styles.streamIcon} />
80
+ <span className={styles.streamName}>System</span>
81
+ </button>
82
+
83
+ {/* Named streams */}
84
+ {loading && sortedStreams.length === 0 && (
85
+ <div className={styles.loading}>Loading...</div>
86
+ )}
87
+ {!loading && streamsLoadError && (
88
+ <div className={styles.emptyState} data-testid="stream-list-error">Unable to load streams</div>
89
+ )}
90
+ {!loading && !streamsLoadError && streamsLoadedOnce && sortedStreams.length === 0 && (
91
+ <div className={styles.emptyState}>No streams</div>
92
+ )}
93
+ {sortedStreams.map((stream) => {
94
+ const isSelected = selectedStreamId === stream.id;
95
+ return (
96
+ <button
97
+ key={stream.id}
98
+ type="button"
99
+ className={`${styles.streamRow}${isSelected ? ` ${styles.selected}` : ""}`}
100
+ onClick={() => handleStreamClick(stream.id)}
101
+ data-testid={`stream-list-row-${stream.id}`}
102
+ aria-current={isSelected ? "page" : undefined}
103
+ >
104
+ <Radio size={ICON_SIZE} className={styles.streamIcon} />
105
+ <span className={styles.streamName}>{stream.name}</span>
106
+ {stream.subscriberCount > 0 && (
107
+ <span className={styles.subscriberBadge}>{stream.subscriberCount}</span>
108
+ )}
109
+ </button>
110
+ );
111
+ })}
112
+ </div>
113
+ );
114
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Stream sidebar and detail panel components.
3
+ *
4
+ * @module streams
5
+ */
6
+
7
+ export { StreamList } from "./StreamList.js";
8
+ export type { StreamListProps } from "./StreamList.js";
9
+ export { StreamDetailPanel } from "./StreamDetailPanel.js";
10
+ export type { StreamDetailPanelProps } from "./StreamDetailPanel.js";
@@ -0,0 +1,118 @@
1
+ @use '../../styles/mixins' as *;
2
+
3
+ // =============================================================================
4
+ // AgentToolCard — subagent spawn/poll card styles
5
+ // =============================================================================
6
+
7
+ // --- Agent type / model badges ---
8
+
9
+ .badgePill {
10
+ display: inline-flex;
11
+ align-items: center;
12
+ padding: 1px 6px;
13
+ border-radius: var(--radius-sm);
14
+ font-size: 10px;
15
+ font-family: var(--font-mono);
16
+ font-weight: var(--font-weight-medium);
17
+ background: rgba(45, 212, 191, 0.12);
18
+ color: var(--accent-teal, #2dd4bf);
19
+ flex-shrink: 0;
20
+ }
21
+
22
+ .modelBadge {
23
+ display: inline-flex;
24
+ align-items: center;
25
+ padding: 1px 6px;
26
+ border-radius: var(--radius-sm);
27
+ font-size: 10px;
28
+ font-family: var(--font-mono);
29
+ background: var(--bg-inset);
30
+ color: var(--text-tertiary);
31
+ flex-shrink: 0;
32
+ }
33
+
34
+ // --- Background indicator ---
35
+
36
+ .backgroundBadge {
37
+ display: inline-flex;
38
+ align-items: center;
39
+ gap: 3px;
40
+ font-size: 10px;
41
+ font-family: var(--font-mono);
42
+ color: var(--text-tertiary);
43
+ flex-shrink: 0;
44
+ }
45
+
46
+ .backgroundDot {
47
+ font-size: 8px;
48
+ color: var(--accent-teal, #2dd4bf);
49
+ }
50
+
51
+ .backgroundDotPulsing {
52
+ font-size: 8px;
53
+ color: var(--accent-teal, #2dd4bf);
54
+ animation: agentPulse 1.5s ease-in-out infinite;
55
+ }
56
+
57
+ @keyframes agentPulse {
58
+ 0%, 100% { opacity: 0.4; }
59
+ 50% { opacity: 1; }
60
+ }
61
+
62
+ // --- Description line ---
63
+
64
+ .description {
65
+ font-size: var(--font-size-sm);
66
+ color: var(--text-secondary);
67
+ padding: var(--space-xs) 0 0;
68
+ line-height: 1.4;
69
+ }
70
+
71
+ // --- Status line (read_agent metadata) ---
72
+
73
+ .statusLine {
74
+ display: flex;
75
+ align-items: center;
76
+ gap: var(--space-sm);
77
+ font-size: 10px;
78
+ font-family: var(--font-mono);
79
+ color: var(--text-tertiary);
80
+ padding: var(--space-xs) 0 0;
81
+ }
82
+
83
+ .statusCompleted {
84
+ font-weight: var(--font-weight-bold);
85
+ color: var(--accent-green);
86
+ }
87
+
88
+ .statusRunning {
89
+ font-weight: var(--font-weight-bold);
90
+ color: var(--accent-teal, #2dd4bf);
91
+ animation: agentPulse 1.5s ease-in-out infinite;
92
+ }
93
+
94
+ .statusError {
95
+ font-weight: var(--font-weight-bold);
96
+ color: var(--accent-red);
97
+ }
98
+
99
+ // --- Prompt section (collapsible) ---
100
+
101
+ .promptToggle {
102
+ display: flex;
103
+ align-items: center;
104
+ gap: var(--space-xs);
105
+ background: none;
106
+ border: none;
107
+ padding: 2px 0;
108
+ margin-top: var(--space-xs);
109
+ color: var(--text-tertiary);
110
+ font-size: 11px;
111
+ font-family: var(--font-mono);
112
+ cursor: pointer;
113
+ transition: color var(--transition-fast);
114
+
115
+ &:hover {
116
+ color: var(--text-secondary);
117
+ }
118
+ }