@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.
- package/.rush/temp/{05ec67b10f932bdbe295aab3f4465cf0d26cb485.tar.log → 2cba8bf698d83f55d8c7bde4c1f1ab17c9392271.tar.log} +78 -80
- package/.rush/temp/{05ec67b10f932bdbe295aab3f4465cf0d26cb485.untar.log → 2cba8bf698d83f55d8c7bde4c1f1ab17c9392271.untar.log} +2 -2
- package/.rush/temp/chunked-rush-logs/web-components._phase_build.chunks.jsonl +6 -6
- package/.rush/temp/chunked-rush-logs/web-components._phase_test.chunks.jsonl +25 -23
- package/.rush/temp/{b47d67cd3e2d79d0da7f9aef2eb425725d6d2f61.tar.log → f7dc40a6d2b535279358eddd5c0cd5f4e522416c.tar.log} +2 -2
- package/.rush/temp/{b47d67cd3e2d79d0da7f9aef2eb425725d6d2f61.untar.log → f7dc40a6d2b535279358eddd5c0cd5f4e522416c.untar.log} +2 -2
- package/.rush/temp/operation/_phase_build/all.log +6 -6
- package/.rush/temp/operation/_phase_build/log-chunks.jsonl +6 -6
- package/.rush/temp/operation/_phase_build/state.json +1 -1
- package/.rush/temp/operation/_phase_test/all.log +25 -23
- package/.rush/temp/operation/_phase_test/log-chunks.jsonl +25 -23
- package/.rush/temp/operation/_phase_test/state.json +1 -1
- package/README.md +2 -2
- package/dist/index.css +1 -1
- package/dist/index.js +7577 -7373
- package/package.json +2 -2
- package/rush-logs/web-components._phase_build.cache.log +1 -1
- package/rush-logs/web-components._phase_build.log +6 -6
- package/rush-logs/web-components._phase_test.cache.log +1 -1
- package/rush-logs/web-components._phase_test.log +25 -23
- package/src/components/display/EventRenderer.stories.tsx +44 -0
- package/src/components/display/EventRenderer.tsx +8 -2
- package/src/components/layout/AppNav.stories.tsx +5 -5
- package/src/components/layout/AppNav.tsx +8 -4
- package/src/components/panels/KeyboardShortcutsPanel.stories.tsx +1 -1
- package/src/components/panels/KeyboardShortcutsPanel.tsx +1 -1
- package/src/components/streams/CoordinationList.module.scss +137 -0
- package/src/components/streams/CoordinationList.stories.tsx +95 -0
- package/src/components/streams/CoordinationList.tsx +153 -0
- package/src/components/streams/StreamDetailPanel.module.scss +30 -0
- package/src/components/streams/StreamDetailPanel.stories.tsx +3 -0
- package/src/components/streams/StreamDetailPanel.tsx +58 -24
- package/src/components/streams/index.ts +3 -3
- package/src/hooks/types.ts +9 -2
- package/src/index.ts +4 -4
- package/src/mocks/MockGrackleProvider.tsx +15 -3
- package/src/mocks/mockData.ts +4 -0
- package/src/mocks/mockStreamsData.ts +80 -0
- package/src/utils/navigation.ts +3 -5
- package/src/utils/streamCoordination.test.ts +88 -0
- package/src/utils/streamCoordination.ts +108 -0
- package/temp/build/lint/_eslint-5eVG3S6w.json +32 -20
- package/src/components/streams/StreamList.module.scss +0 -92
- package/src/components/streams/StreamList.stories.tsx +0 -99
- 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.
|
|
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.
|
|
49
|
+
"@grackle-ai/common": "0.115.0"
|
|
50
50
|
},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@rushstack/heft": "1.2.7",
|
|
@@ -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
|
-
✓
|
|
10
|
+
✓ 2718 modules transformed.
|
|
11
11
|
rendering chunks...
|
|
12
12
|
computing gzip size...
|
|
13
|
-
dist/index.css
|
|
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,
|
|
16
|
-
✓ built in 5.
|
|
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 (
|
|
19
|
-
-------------------- Finished (
|
|
18
|
+
---- build finished (75.689s) ----
|
|
19
|
+
-------------------- Finished (75.692s) --------------------
|
|
@@ -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/
|
|
9
|
-
✓ src/utils/
|
|
10
|
-
✓ src/utils/dashboard.test.ts (4 tests)
|
|
11
|
-
✓ src/utils/route-config.test.ts (23 tests)
|
|
12
|
-
✓ src/utils/
|
|
13
|
-
✓ src/utils/breadcrumbs.test.ts (18 tests)
|
|
14
|
-
✓ src/
|
|
15
|
-
✓ src/
|
|
16
|
-
✓ src/
|
|
17
|
-
✓ src/components/
|
|
18
|
-
✓ src/components/
|
|
19
|
-
✓ src/
|
|
20
|
-
✓ src/
|
|
21
|
-
✓ src/
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
|
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 (
|
|
124
|
-
-------------------- Finished (
|
|
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
|
-
|
|
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
|
-
|
|
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: /
|
|
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: "
|
|
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: /
|
|
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: "
|
|
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: "
|
|
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: "
|
|
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("
|
|
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: "
|
|
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
|
+
};
|