@grackle-ai/web-components 0.113.0 → 0.115.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/.rush/temp/{05ec67b10f932bdbe295aab3f4465cf0d26cb485.tar.log → 2cba8bf698d83f55d8c7bde4c1f1ab17c9392271.tar.log} +78 -80
  2. package/.rush/temp/{05ec67b10f932bdbe295aab3f4465cf0d26cb485.untar.log → 2cba8bf698d83f55d8c7bde4c1f1ab17c9392271.untar.log} +2 -2
  3. package/.rush/temp/chunked-rush-logs/web-components._phase_build.chunks.jsonl +6 -6
  4. package/.rush/temp/chunked-rush-logs/web-components._phase_test.chunks.jsonl +25 -23
  5. package/.rush/temp/{b47d67cd3e2d79d0da7f9aef2eb425725d6d2f61.tar.log → f7dc40a6d2b535279358eddd5c0cd5f4e522416c.tar.log} +2 -2
  6. package/.rush/temp/{b47d67cd3e2d79d0da7f9aef2eb425725d6d2f61.untar.log → f7dc40a6d2b535279358eddd5c0cd5f4e522416c.untar.log} +2 -2
  7. package/.rush/temp/operation/_phase_build/all.log +6 -6
  8. package/.rush/temp/operation/_phase_build/log-chunks.jsonl +6 -6
  9. package/.rush/temp/operation/_phase_build/state.json +1 -1
  10. package/.rush/temp/operation/_phase_test/all.log +25 -23
  11. package/.rush/temp/operation/_phase_test/log-chunks.jsonl +25 -23
  12. package/.rush/temp/operation/_phase_test/state.json +1 -1
  13. package/README.md +2 -2
  14. package/dist/index.css +1 -1
  15. package/dist/index.js +7577 -7373
  16. package/package.json +2 -2
  17. package/rush-logs/web-components._phase_build.cache.log +1 -1
  18. package/rush-logs/web-components._phase_build.log +6 -6
  19. package/rush-logs/web-components._phase_test.cache.log +1 -1
  20. package/rush-logs/web-components._phase_test.log +25 -23
  21. package/src/components/display/EventRenderer.stories.tsx +44 -0
  22. package/src/components/display/EventRenderer.tsx +8 -2
  23. package/src/components/layout/AppNav.stories.tsx +5 -5
  24. package/src/components/layout/AppNav.tsx +8 -4
  25. package/src/components/panels/KeyboardShortcutsPanel.stories.tsx +1 -1
  26. package/src/components/panels/KeyboardShortcutsPanel.tsx +1 -1
  27. package/src/components/streams/CoordinationList.module.scss +137 -0
  28. package/src/components/streams/CoordinationList.stories.tsx +95 -0
  29. package/src/components/streams/CoordinationList.tsx +153 -0
  30. package/src/components/streams/StreamDetailPanel.module.scss +30 -0
  31. package/src/components/streams/StreamDetailPanel.stories.tsx +3 -0
  32. package/src/components/streams/StreamDetailPanel.tsx +58 -24
  33. package/src/components/streams/index.ts +3 -3
  34. package/src/hooks/types.ts +9 -2
  35. package/src/index.ts +4 -4
  36. package/src/mocks/MockGrackleProvider.tsx +15 -3
  37. package/src/mocks/mockData.ts +4 -0
  38. package/src/mocks/mockStreamsData.ts +80 -0
  39. package/src/utils/navigation.ts +3 -5
  40. package/src/utils/streamCoordination.test.ts +88 -0
  41. package/src/utils/streamCoordination.ts +108 -0
  42. package/temp/build/lint/_eslint-5eVG3S6w.json +32 -20
  43. package/src/components/streams/StreamList.module.scss +0 -92
  44. package/src/components/streams/StreamList.stories.tsx +0 -99
  45. package/src/components/streams/StreamList.tsx +0 -114
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@grackle-ai/web-components",
3
- "version": "0.113.0",
3
+ "version": "0.115.0",
4
4
  "description": "Presentational React component library for the Grackle web UI",
5
5
  "license": "MIT",
6
6
  "sideEffects": [
@@ -46,7 +46,7 @@
46
46
  "remark-gfm": "^4.0.0",
47
47
  "lucide-react": "~0.474.0",
48
48
  "react-router": "^7.0.0",
49
- "@grackle-ai/common": "0.113.0"
49
+ "@grackle-ai/common": "0.115.0"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@rushstack/heft": "1.2.7",
@@ -1,4 +1,4 @@
1
1
  Build cache hit.
2
- Cache key: 05ec67b10f932bdbe295aab3f4465cf0d26cb485
2
+ Cache key: 2cba8bf698d83f55d8c7bde4c1f1ab17c9392271
3
3
  Clearing cached folders: dist, storybook-static, .rush/temp/operation/_phase_build
4
4
  Successfully restored output from the build cache.
@@ -7,13 +7,13 @@ Invoking: heft run --only build -- --clean
7
7
  [build:lint] Using ESLint version 9.39.4
8
8
  vite v6.4.2 building for production...
9
9
  transforming...
10
- 2716 modules transformed.
10
+ 2718 modules transformed.
11
11
  rendering chunks...
12
12
  computing gzip size...
13
- dist/index.css 158.47 kB │ gzip: 20.60 kB
13
+ dist/index.css 159.65 kB │ gzip: 20.78 kB
14
14
  dist/McpAppWidget-CSX2W2Vb.js 205.28 kB │ gzip: 47.19 kB
15
- dist/index.js 1,385.24 kB │ gzip: 354.56 kB
16
- ✓ built in 5.92s
15
+ dist/index.js 1,392.84 kB │ gzip: 356.12 kB
16
+ ✓ built in 5.78s
17
17
  [build:vite-build] Vite build completed.
18
- ---- build finished (77.344s) ----
19
- -------------------- Finished (77.347s) --------------------
18
+ ---- build finished (75.689s) ----
19
+ -------------------- Finished (75.692s) --------------------
@@ -1,4 +1,4 @@
1
1
  Build cache hit.
2
- Cache key: b47d67cd3e2d79d0da7f9aef2eb425725d6d2f61
2
+ Cache key: f7dc40a6d2b535279358eddd5c0cd5f4e522416c
3
3
  Clearing cached folders: .rush/temp/operation/_phase_test
4
4
  Successfully restored output from the build cache.
@@ -5,29 +5,31 @@ The provided list of phases does not contain all phase dependencies. You may nee
5
5
 
6
6
  RUN v3.2.4 /home/runner/work/grackle/grackle/packages/web-components
7
7
 
8
- ✓ src/utils/sessionEvents.test.ts (14 tests) 36ms
9
- ✓ src/utils/eventContent.test.ts (38 tests) 110ms
10
- ✓ src/utils/dashboard.test.ts (4 tests) 7ms
11
- ✓ src/utils/route-config.test.ts (23 tests) 21ms
12
- ✓ src/utils/scrollUtils.test.ts (11 tests) 49ms
13
- ✓ src/utils/breadcrumbs.test.ts (18 tests) 88ms
14
- ✓ src/components/tools/classifyTool.test.ts (6 tests) 17ms
15
- ✓ src/utils/assetUrl.test.ts (3 tests) 33ms
16
- ✓ src/components/tools/toolCardHelpers.test.ts (10 tests) 29ms
17
- ✓ src/components/display/extractText.test.tsx (8 tests) 31ms
18
- ✓ src/components/editable/useEditableField.test.tsx (17 tests) 273ms
19
- ✓ src/hooks/useEventSelection.test.ts (13 tests) 121ms
20
- ✓ src/components/notifications/UpdateBanner.test.tsx (4 tests) 155ms
21
- ✓ src/utils/grackleHostStyleVariables.test.ts (2 tests) 306ms
22
- src/components/display/McpAppWidget.test.tsx (3 tests) 383ms
23
-
24
- Test Files 15 passed (15)
25
- Tests 174 passed (174)
26
- Start at 03:06:05
27
- Duration 16.86s (transform 3.49s, setup 0ms, collect 18.02s, tests 1.66s, environment 13.18s, prepare 5.11s)
8
+ ✓ src/utils/eventContent.test.ts (38 tests) 39ms
9
+ ✓ src/utils/sessionEvents.test.ts (14 tests) 48ms
10
+ ✓ src/utils/dashboard.test.ts (4 tests) 26ms
11
+ ✓ src/utils/route-config.test.ts (23 tests) 48ms
12
+ ✓ src/utils/streamCoordination.test.ts (10 tests) 29ms
13
+ ✓ src/utils/breadcrumbs.test.ts (18 tests) 70ms
14
+ ✓ src/utils/scrollUtils.test.ts (11 tests) 42ms
15
+ ✓ src/components/tools/classifyTool.test.ts (6 tests) 7ms
16
+ ✓ src/utils/assetUrl.test.ts (3 tests) 16ms
17
+ ✓ src/components/tools/toolCardHelpers.test.ts (10 tests) 26ms
18
+ ✓ src/components/display/extractText.test.tsx (8 tests) 7ms
19
+ ✓ src/components/editable/useEditableField.test.tsx (17 tests) 140ms
20
+ ✓ src/hooks/useEventSelection.test.ts (13 tests) 185ms
21
+ ✓ src/components/display/McpAppWidget.test.tsx (3 tests) 635ms
22
+ ✓ McpAppWidget > mounts an iframe host for the widget 401ms
23
+ ✓ src/components/notifications/UpdateBanner.test.tsx (4 tests) 152ms
24
+ src/utils/grackleHostStyleVariables.test.ts (2 tests) 300ms
25
+
26
+ Test Files 16 passed (16)
27
+ Tests 184 passed (184)
28
+ Start at 13:23:17
29
+ Duration 16.16s (transform 2.79s, setup 0ms, collect 18.18s, tests 1.77s, environment 12.06s, prepare 6.31s)
28
30
 
29
31
  [test:vitest] Vitest completed.
30
- [test:storybook-test] Starting Storybook static server on port 33461...
32
+ [test:storybook-test] Starting Storybook static server on port 35329...
31
33
  [test:storybook-test] Storybook server ready. Running interaction tests...
32
34
  jest-haste-map: duplicate manual mock found: adapter-manager
33
35
  The following files share their name; please delete one of them:
@@ -120,5 +122,5 @@ jest-haste-map: duplicate manual mock found: utils/network
120
122
  * <rootDir>/packages/server/src/__mocks__/utils/network.ts
121
123
 
122
124
  [test:storybook-test] Storybook interaction tests completed.
123
- ---- test finished (118.259s) ----
124
- -------------------- Finished (118.269s) --------------------
125
+ ---- test finished (113.095s) ----
126
+ -------------------- Finished (113.1s) --------------------
@@ -192,6 +192,50 @@ export const WidgetEvent: Story = {
192
192
  },
193
193
  };
194
194
 
195
+ /** Agent-authored widget (#1239): dynamic rendererKind + inline-script body + props. */
196
+ export const AgentWidgetEvent: Story = {
197
+ args: {
198
+ event: makeEvent({
199
+ eventType: "widget",
200
+ content: JSON.stringify({
201
+ resourceUri: "ui://grackle/abc123",
202
+ toolName: "widget_render",
203
+ rendererKind: "mcp-app-html",
204
+ widgetId: "abc123",
205
+ version: 2,
206
+ html: "<!doctype html><html><body><div class=\"card\">agent widget</div><script>void 0;</script></body></html>",
207
+ csp: { resourceDomains: ["http://localhost:6007"], connectDomains: ["http://localhost:6007"], allowInlineScripts: true },
208
+ toolInput: { count: 3 },
209
+ toolResult: { content: [{ type: "text", text: "ok" }] },
210
+ }),
211
+ }),
212
+ sandboxProxyUrl: "http://localhost:6007/sandbox.html",
213
+ },
214
+ play: async ({ canvas }) => {
215
+ await expect(await canvas.findByTestId("mcp-app-widget")).toBeInTheDocument();
216
+ },
217
+ };
218
+
219
+ /** An unknown rendererKind falls back to the default event card (no crash). */
220
+ export const UnknownRendererKindWidget: Story = {
221
+ args: {
222
+ event: makeEvent({
223
+ eventType: "widget",
224
+ content: JSON.stringify({
225
+ resourceUri: "",
226
+ toolName: "widget_show",
227
+ rendererKind: "adaptive-card",
228
+ html: "<div>future</div>",
229
+ }),
230
+ }),
231
+ sandboxProxyUrl: "http://localhost:6007/sandbox.html",
232
+ },
233
+ play: async ({ canvas }) => {
234
+ // Unsupported renderer: McpAppWidget is NOT mounted; the raw content shows instead.
235
+ await expect(canvas.queryByTestId("mcp-app-widget")).toBeNull();
236
+ },
237
+ };
238
+
195
239
  /** User input events render as markdown (bold, lists, inline code) inside the bubble. */
196
240
  export const UserMessageMarkdown: Story = {
197
241
  args: {
@@ -225,14 +225,20 @@ export function EventRenderer({ event, toolUseCtx, settled, sandboxProxyUrl }: P
225
225
  }
226
226
  let payload: {
227
227
  html?: string;
228
- csp?: McpUiResourceCsp;
228
+ rendererKind?: string;
229
+ // `allowInlineScripts` is a Grackle extension to the upstream CSP type
230
+ // (agent-authored widgets, #1239); it is forwarded verbatim to the sandbox.
231
+ csp?: McpUiResourceCsp & { allowInlineScripts?: boolean };
229
232
  toolInput?: Record<string, unknown>;
230
233
  toolResult?: CallToolResult;
231
234
  } = {};
232
235
  try {
233
236
  payload = JSON.parse(event.content) as typeof payload;
234
237
  } catch { /* malformed widget payload — fall back */ }
235
- if (!payload.html) {
238
+ // Dispatch on rendererKind (default "mcp-app-html" for back-compat). This
239
+ // switch is the seam for future declarative renderers (e.g. adaptive-card).
240
+ const rendererKind: string = payload.rendererKind ?? "mcp-app-html";
241
+ if (rendererKind !== "mcp-app-html" || !payload.html) {
236
242
  return <DefaultEvent content={event.content} />;
237
243
  }
238
244
  return (
@@ -17,7 +17,7 @@ type Story = StoryObj<typeof meta>;
17
17
  export const AllTabsRendered: Story = {
18
18
  play: async ({ canvas }) => {
19
19
  await expect(canvas.getByRole("tab", { name: /Dashboard/ })).toBeInTheDocument();
20
- await expect(canvas.getByRole("tab", { name: /Sessions/ })).toBeInTheDocument();
20
+ await expect(canvas.getByRole("tab", { name: /Root/ })).toBeInTheDocument();
21
21
  await expect(canvas.getByRole("tab", { name: /Tasks/ })).toBeInTheDocument();
22
22
  await expect(canvas.getByRole("tab", { name: /Environments/ })).toBeInTheDocument();
23
23
  await expect(canvas.getByRole("tab", { name: /Knowledge/ })).toBeInTheDocument();
@@ -30,14 +30,14 @@ export const CoreOnlyTabs: Story = {
30
30
  args: {
31
31
  tabs: [
32
32
  { view: "dashboard", label: "Dashboard", icon: <Home size={ICON_LG} />, route: HOME_URL, testId: "sidebar-tab-dashboard" },
33
- { view: "chat", label: "Sessions", icon: <MessageSquare size={ICON_LG} />, route: CHAT_URL, testId: "sidebar-tab-chat" },
33
+ { view: "chat", label: "Root", icon: <MessageSquare size={ICON_LG} />, route: CHAT_URL, testId: "sidebar-tab-chat" },
34
34
  { view: "environments", label: "Environments", icon: <Monitor size={ICON_LG} />, route: ENVIRONMENTS_URL, testId: "sidebar-tab-environments" },
35
35
  { view: "settings", label: "Settings", icon: <Settings size={ICON_LG} />, route: SETTINGS_CREDENTIALS_URL, testId: "sidebar-tab-settings" },
36
36
  ],
37
37
  },
38
38
  play: async ({ canvas }) => {
39
39
  await expect(canvas.getByRole("tab", { name: /Dashboard/ })).toBeInTheDocument();
40
- await expect(canvas.getByRole("tab", { name: /Sessions/ })).toBeInTheDocument();
40
+ await expect(canvas.getByRole("tab", { name: /Root/ })).toBeInTheDocument();
41
41
  await expect(canvas.getByRole("tab", { name: /Environments/ })).toBeInTheDocument();
42
42
  await expect(canvas.getByRole("tab", { name: /Settings/ })).toBeInTheDocument();
43
43
  await expect(canvas.queryByRole("tab", { name: /Tasks/ })).not.toBeInTheDocument();
@@ -51,7 +51,7 @@ export const AllTabsExplicit: Story = {
51
51
  args: {
52
52
  tabs: [
53
53
  { view: "dashboard", label: "Dashboard", icon: <Home size={ICON_LG} />, route: HOME_URL, testId: "sidebar-tab-dashboard" },
54
- { view: "chat", label: "Sessions", icon: <MessageSquare size={ICON_LG} />, route: CHAT_URL, testId: "sidebar-tab-chat" },
54
+ { view: "chat", label: "Root", icon: <MessageSquare size={ICON_LG} />, route: CHAT_URL, testId: "sidebar-tab-chat" },
55
55
  { view: "tasks", label: "Tasks", icon: <ClipboardList size={ICON_LG} />, route: TASKS_URL, testId: "sidebar-tab-tasks" },
56
56
  { view: "environments", label: "Environments", icon: <Monitor size={ICON_LG} />, route: ENVIRONMENTS_URL, testId: "sidebar-tab-environments" },
57
57
  { view: "knowledge", label: "Knowledge", icon: <Brain size={ICON_LG} />, route: KNOWLEDGE_URL, testId: "sidebar-tab-knowledge" },
@@ -78,7 +78,7 @@ export const SettingsPinnedRight: Story = {
78
78
  args: {
79
79
  tabs: [
80
80
  { view: "dashboard", label: "Dashboard", icon: <Home size={ICON_LG} />, route: HOME_URL, testId: "sidebar-tab-dashboard" },
81
- { view: "chat", label: "Sessions", icon: <MessageSquare size={ICON_LG} />, route: CHAT_URL, testId: "sidebar-tab-chat" },
81
+ { view: "chat", label: "Root", icon: <MessageSquare size={ICON_LG} />, route: CHAT_URL, testId: "sidebar-tab-chat" },
82
82
  { view: "environments", label: "Environments", icon: <Monitor size={ICON_LG} />, route: ENVIRONMENTS_URL, testId: "sidebar-tab-environments" },
83
83
  { view: "settings", label: "Settings", icon: <Settings size={ICON_LG} />, route: SETTINGS_CREDENTIALS_URL, testId: "sidebar-tab-settings", align: "end" },
84
84
  { view: "tasks", label: "Tasks", icon: <ClipboardList size={ICON_LG} />, route: TASKS_URL, testId: "sidebar-tab-tasks" },
@@ -1,13 +1,13 @@
1
1
  import { useCallback, useMemo, useRef, type JSX, type KeyboardEvent, type ReactNode } from "react";
2
2
  import { useLocation } from "react-router";
3
- import { Brain, ClipboardList, Home, MessageSquare, Monitor, Search, Settings } from "lucide-react";
4
- import { CHAT_URL, ENVIRONMENTS_URL, FINDINGS_URL, HOME_URL, KNOWLEDGE_URL, SETTINGS_URL, SETTINGS_CREDENTIALS_URL, TASKS_URL, useAppNavigate } from "../../utils/navigation.js";
3
+ import { Brain, ClipboardList, Home, MessageSquare, Monitor, Network, Search, Settings } from "lucide-react";
4
+ import { CHAT_URL, COORDINATION_URL, ENVIRONMENTS_URL, FINDINGS_URL, HOME_URL, KNOWLEDGE_URL, SETTINGS_URL, SETTINGS_CREDENTIALS_URL, TASKS_URL, useAppNavigate } from "../../utils/navigation.js";
5
5
  import { ICON_LG } from "../../utils/iconSize.js";
6
6
  import { Tooltip } from "../display/Tooltip.js";
7
7
  import styles from "./AppNav.module.scss";
8
8
 
9
9
  /** Application view identifiers. */
10
- export type AppView = "dashboard" | "chat" | "tasks" | "environments" | "knowledge" | "findings" | "settings";
10
+ export type AppView = "dashboard" | "chat" | "tasks" | "environments" | "knowledge" | "findings" | "coordination" | "settings";
11
11
 
12
12
  /** Tab definition for the application navigation bar. */
13
13
  export interface AppTab {
@@ -36,9 +36,10 @@ export const TABS: AppTab[] = [
36
36
  { view: "dashboard", label: "Dashboard", icon: <Home size={ICON_LG} />, route: HOME_URL, testId: "sidebar-tab-dashboard", order: 0 },
37
37
  { view: "tasks", label: "Tasks", icon: <ClipboardList size={ICON_LG} />, route: TASKS_URL, testId: "sidebar-tab-tasks", order: 1 },
38
38
  { view: "environments", label: "Environments", icon: <Monitor size={ICON_LG} />, route: ENVIRONMENTS_URL, testId: "sidebar-tab-environments", order: 2 },
39
- { view: "chat", label: "Sessions", icon: <MessageSquare size={ICON_LG} />, route: CHAT_URL, testId: "sidebar-tab-chat", order: 3 },
39
+ { view: "chat", label: "Root", icon: <MessageSquare size={ICON_LG} />, route: CHAT_URL, testId: "sidebar-tab-chat", order: 3 },
40
40
  { view: "findings", label: "Findings", icon: <Search size={ICON_LG} />, route: FINDINGS_URL, testId: "sidebar-tab-findings", order: 4 },
41
41
  { view: "knowledge", label: "Knowledge", icon: <Brain size={ICON_LG} />, route: KNOWLEDGE_URL, testId: "sidebar-tab-knowledge", order: 5 },
42
+ { view: "coordination", label: "Coordination", icon: <Network size={ICON_LG} />, route: COORDINATION_URL, testId: "sidebar-tab-coordination", order: 6 },
42
43
  { view: "settings", label: "Settings", icon: <Settings size={ICON_LG} />, route: SETTINGS_CREDENTIALS_URL, testId: "sidebar-tab-settings", align: "end" },
43
44
  ];
44
45
 
@@ -47,6 +48,9 @@ export function getActiveView(pathname: string): AppView {
47
48
  if (pathname === HOME_URL || pathname === "/") {
48
49
  return "dashboard";
49
50
  }
51
+ if (pathname.startsWith(COORDINATION_URL)) {
52
+ return "coordination";
53
+ }
50
54
  if (pathname.startsWith("/chat") || pathname.startsWith("/sessions")) {
51
55
  return "chat";
52
56
  }
@@ -25,7 +25,7 @@ export const AllCategoriesRendered: Story = {
25
25
  await expect(canvas.getByText("Workspace Page")).toBeInTheDocument();
26
26
  await expect(canvas.getByText("Navigation Lists")).toBeInTheDocument();
27
27
  await expect(canvas.getByText("Editing")).toBeInTheDocument();
28
- await expect(canvas.getByText("Sessions")).toBeInTheDocument();
28
+ await expect(canvas.getByText("Root")).toBeInTheDocument();
29
29
  },
30
30
  };
31
31
 
@@ -66,7 +66,7 @@ const SHORTCUT_GROUPS: ShortcutGroup[] = [
66
66
  ],
67
67
  },
68
68
  {
69
- title: "Sessions",
69
+ title: "Root",
70
70
  shortcuts: [
71
71
  { keys: ["Ctrl/Cmd", "Enter"], description: "Send message (when the composer is focused)" },
72
72
  { keys: ["Enter"], description: "Insert a new line in the message composer" },
@@ -0,0 +1,137 @@
1
+ @use '../../styles/mixins' as *;
2
+
3
+ // =============================================================================
4
+ // CoordinationList — read-only IPC stream inventory (Coordination tab)
5
+ // =============================================================================
6
+
7
+ .container {
8
+ flex: 1;
9
+ overflow-y: auto;
10
+ padding: var(--space-sm) 0;
11
+ }
12
+
13
+ .header {
14
+ display: flex;
15
+ align-items: center;
16
+ justify-content: space-between;
17
+ padding: var(--space-sm) var(--space-md);
18
+ border-bottom: 1px solid var(--border-subtle);
19
+ }
20
+
21
+ .title {
22
+ font-size: var(--font-size-md);
23
+ font-weight: var(--font-weight-bold);
24
+ color: var(--text-primary);
25
+ }
26
+
27
+ .headerActions {
28
+ display: flex;
29
+ align-items: center;
30
+ gap: var(--space-md);
31
+ }
32
+
33
+ .internalsToggle {
34
+ display: inline-flex;
35
+ align-items: center;
36
+ gap: var(--space-xs);
37
+ font-size: var(--font-size-sm);
38
+ color: var(--text-secondary);
39
+ cursor: pointer;
40
+ user-select: none;
41
+ }
42
+
43
+ .refreshButton {
44
+ background: none;
45
+ border: none;
46
+ color: var(--text-secondary);
47
+ cursor: pointer;
48
+ padding: 2px;
49
+ display: flex;
50
+ align-items: center;
51
+ border-radius: var(--radius-sm);
52
+
53
+ &:hover {
54
+ color: var(--text-primary);
55
+ background: var(--bg-overlay);
56
+ }
57
+ }
58
+
59
+ .state {
60
+ padding: var(--space-lg) var(--space-md);
61
+ font-size: var(--font-size-sm);
62
+ color: var(--text-disabled);
63
+ text-align: center;
64
+ }
65
+
66
+ .group {
67
+ margin-top: var(--space-sm);
68
+ }
69
+
70
+ .groupHeader {
71
+ @include section-label;
72
+ padding: var(--space-xs) var(--space-md);
73
+ }
74
+
75
+ .row {
76
+ @include hover-accent;
77
+ display: flex;
78
+ align-items: center;
79
+ gap: var(--space-sm);
80
+ width: 100%;
81
+ padding: var(--space-xs) var(--space-md);
82
+ background: none;
83
+ border: none;
84
+ text-align: left;
85
+ cursor: pointer;
86
+ min-height: 34px;
87
+
88
+ &.selected {
89
+ background: var(--bg-overlay);
90
+ }
91
+ }
92
+
93
+ .kindBadge {
94
+ display: inline-flex;
95
+ align-items: center;
96
+ gap: 4px;
97
+ flex-shrink: 0;
98
+ font-size: 11px;
99
+ font-weight: var(--font-weight-medium);
100
+ padding: 1px 6px;
101
+ border-radius: var(--radius-full);
102
+ border: 1px solid var(--border-subtle);
103
+ }
104
+
105
+ .kindChatroom {
106
+ color: var(--accent-green);
107
+ background: var(--accent-green-dim);
108
+ border-color: var(--accent-green);
109
+ }
110
+
111
+ .kindPipe {
112
+ color: var(--accent-yellow);
113
+ background: var(--bg-inset);
114
+ border-color: var(--accent-yellow);
115
+ }
116
+
117
+ .kindChannel {
118
+ color: var(--accent-blue);
119
+ background: var(--accent-blue-dim);
120
+ border-color: var(--accent-blue);
121
+ }
122
+
123
+ .streamName {
124
+ flex: 1;
125
+ font-size: 13px;
126
+ color: var(--text-primary);
127
+ overflow: hidden;
128
+ text-overflow: ellipsis;
129
+ white-space: nowrap;
130
+ }
131
+
132
+ .meta {
133
+ flex-shrink: 0;
134
+ font-size: 11px;
135
+ color: var(--text-tertiary);
136
+ white-space: nowrap;
137
+ }
@@ -0,0 +1,95 @@
1
+ import type { Meta, StoryObj } from "@storybook/react";
2
+ import { expect, fn, userEvent } from "@storybook/test";
3
+ import type { Session, StreamData } from "../../hooks/types.js";
4
+ import { CoordinationList } from "./CoordinationList.js";
5
+
6
+ function sub(sessionId: string): StreamData["subscribers"][number] {
7
+ return { subscriptionId: `sub-${sessionId}`, sessionId, fd: 3, permission: "rw", deliveryMode: "async", createdBySpawn: false };
8
+ }
9
+ function stream(over: Partial<StreamData> & { id: string; name: string }): StreamData {
10
+ return { subscriberCount: over.subscribers?.length ?? 1, messageBufferDepth: 0, selfEcho: false, subscribers: over.subscribers ?? [sub("s1")], ...over };
11
+ }
12
+ function session(id: string, taskId?: string): Session {
13
+ return { id, environmentId: "env-1", runtime: "claude-code", status: "running", prompt: "", startedAt: "2026-01-01T00:00:00Z", taskId };
14
+ }
15
+
16
+ const sessions: Session[] = [session("s1", "task-1"), session("s2", "task-1"), session("s3")];
17
+ const tasks: { id: string; title: string }[] = [{ id: "task-1", title: "Implement JWT auth" }];
18
+
19
+ const mixedStreams: StreamData[] = [
20
+ stream({ id: "room", name: "agent-chat", selfEcho: true, subscribers: [sub("s1"), sub("s2")], subscriberCount: 2 }),
21
+ stream({ id: "chan", name: "telemetry", subscribers: [sub("s2")] }),
22
+ stream({ id: "orphan", name: "cli-inspector", subscribers: [sub("s3")] }),
23
+ ];
24
+
25
+ const meta: Meta<typeof CoordinationList> = {
26
+ title: "Grackle/Streams/CoordinationList",
27
+ component: CoordinationList,
28
+ args: {
29
+ streams: mixedStreams,
30
+ sessions,
31
+ tasks,
32
+ loading: false,
33
+ loadedOnce: true,
34
+ showInternals: false,
35
+ onToggleInternals: fn(),
36
+ onSelectStream: fn(),
37
+ onRefresh: fn(),
38
+ },
39
+ };
40
+ export default meta;
41
+ type Story = StoryObj<typeof meta>;
42
+
43
+ /** Streams grouped by owning task, with a trailing unattached/external bucket. */
44
+ export const GroupedByTask: Story = {
45
+ play: async ({ canvas }) => {
46
+ await expect(canvas.getByTestId("coordination-list")).toBeInTheDocument();
47
+ await expect(canvas.getByText("Implement JWT auth")).toBeInTheDocument();
48
+ await expect(canvas.getByText(/Unattached/)).toBeInTheDocument();
49
+ await expect(canvas.getByTestId("coordination-row-room")).toBeInTheDocument();
50
+ await expect(canvas.getByTestId("coordination-row-orphan")).toBeInTheDocument();
51
+ },
52
+ };
53
+
54
+ /** Each stream is tagged with its kind badge. */
55
+ export const KindBadges: Story = {
56
+ play: async ({ canvas }) => {
57
+ // mixedStreams has one chatroom and two channels.
58
+ await expect(canvas.getAllByText("Chatroom")).toHaveLength(1);
59
+ await expect(canvas.getAllByText("Channel")).toHaveLength(2);
60
+ },
61
+ };
62
+
63
+ /** Toggling "Show internals" calls back to re-fetch. */
64
+ export const InternalsToggle: Story = {
65
+ play: async ({ canvas, args }) => {
66
+ const toggle = canvas.getByTestId("coordination-show-internals");
67
+ await expect(toggle).not.toBeChecked();
68
+ await userEvent.click(toggle);
69
+ await expect(args.onToggleInternals).toHaveBeenCalledWith(true);
70
+ },
71
+ };
72
+
73
+ /** Clicking a row selects the stream. */
74
+ export const SelectStream: Story = {
75
+ play: async ({ canvas, args }) => {
76
+ await userEvent.click(canvas.getByTestId("coordination-row-chan"));
77
+ await expect(args.onSelectStream).toHaveBeenCalledWith("chan");
78
+ },
79
+ };
80
+
81
+ /** Empty state when there are no streams. */
82
+ export const EmptyState: Story = {
83
+ args: { streams: [] },
84
+ play: async ({ canvas }) => {
85
+ await expect(canvas.getByTestId("coordination-empty")).toBeInTheDocument();
86
+ },
87
+ };
88
+
89
+ /** Error state when the load failed. */
90
+ export const ErrorState: Story = {
91
+ args: { streams: [], loadError: true },
92
+ play: async ({ canvas }) => {
93
+ await expect(canvas.getByTestId("coordination-error")).toBeInTheDocument();
94
+ },
95
+ };