@grackle-ai/web-components 0.113.0 → 0.114.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/{b47d67cd3e2d79d0da7f9aef2eb425725d6d2f61.tar.log → 989dfd9dfc66be6288052dccbf2952ddad9066c6.tar.log} +2 -2
- package/.rush/temp/{05ec67b10f932bdbe295aab3f4465cf0d26cb485.untar.log → 989dfd9dfc66be6288052dccbf2952ddad9066c6.untar.log} +2 -2
- package/.rush/temp/{05ec67b10f932bdbe295aab3f4465cf0d26cb485.tar.log → b1a36bcc314d65fbd843fcff6d3127ebdf827e06.tar.log} +76 -78
- package/.rush/temp/{b47d67cd3e2d79d0da7f9aef2eb425725d6d2f61.untar.log → b1a36bcc314d65fbd843fcff6d3127ebdf827e06.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/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 +1 -1
- 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/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 +30 -18
- 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.114.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.114.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.78 kB │ gzip: 356.09 kB
|
|
16
|
+
✓ built in 5.94s
|
|
17
17
|
[build:vite-build] Vite build completed.
|
|
18
|
-
---- build finished (
|
|
19
|
-
-------------------- Finished (
|
|
18
|
+
---- build finished (75.104s) ----
|
|
19
|
+
-------------------- Finished (75.107s) --------------------
|
|
@@ -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)
|
|
9
|
-
✓ src/utils/eventContent.test.ts (38 tests)
|
|
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/components/tools/toolCardHelpers.test.ts (10 tests)
|
|
17
|
-
✓ src/
|
|
18
|
-
✓ src/components/
|
|
19
|
-
✓ src/hooks/useEventSelection.test.ts (13 tests)
|
|
20
|
-
✓ src/components/
|
|
21
|
-
✓ src/
|
|
22
|
-
✓ src/components/display/McpAppWidget.test.tsx (3 tests)
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
8
|
+
✓ src/utils/sessionEvents.test.ts (14 tests) 67ms
|
|
9
|
+
✓ src/utils/eventContent.test.ts (38 tests) 187ms
|
|
10
|
+
✓ src/utils/dashboard.test.ts (4 tests) 38ms
|
|
11
|
+
✓ src/utils/route-config.test.ts (23 tests) 90ms
|
|
12
|
+
✓ src/utils/streamCoordination.test.ts (10 tests) 29ms
|
|
13
|
+
✓ src/utils/breadcrumbs.test.ts (18 tests) 41ms
|
|
14
|
+
✓ src/utils/scrollUtils.test.ts (11 tests) 41ms
|
|
15
|
+
✓ src/components/tools/classifyTool.test.ts (6 tests) 19ms
|
|
16
|
+
✓ src/components/tools/toolCardHelpers.test.ts (10 tests) 14ms
|
|
17
|
+
✓ src/utils/assetUrl.test.ts (3 tests) 11ms
|
|
18
|
+
✓ src/components/display/extractText.test.tsx (8 tests) 12ms
|
|
19
|
+
✓ src/hooks/useEventSelection.test.ts (13 tests) 244ms
|
|
20
|
+
✓ src/components/editable/useEditableField.test.tsx (17 tests) 231ms
|
|
21
|
+
✓ src/components/notifications/UpdateBanner.test.tsx (4 tests) 169ms
|
|
22
|
+
✓ src/components/display/McpAppWidget.test.tsx (3 tests) 234ms
|
|
23
|
+
✓ src/utils/grackleHostStyleVariables.test.ts (2 tests) 477ms
|
|
24
|
+
✓ grackleHostStyleVariables > always returns the MCP-standard fallback variables 467ms
|
|
25
|
+
|
|
26
|
+
Test Files 16 passed (16)
|
|
27
|
+
Tests 184 passed (184)
|
|
28
|
+
Start at 08:12:34
|
|
29
|
+
Duration 15.79s (transform 2.74s, setup 0ms, collect 15.85s, tests 1.90s, environment 10.61s, prepare 6.00s)
|
|
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 45781...
|
|
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 (111.929s) ----
|
|
126
|
+
-------------------- Finished (111.943s) --------------------
|
|
@@ -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
|
+
};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CoordinationList — read-only inventory of IPC streams for the Coordination tab.
|
|
3
|
+
*
|
|
4
|
+
* Groups streams by the task that owns their subscribers (with a trailing
|
|
5
|
+
* unattached/external bucket), tags each by kind, and offers a "Show internals"
|
|
6
|
+
* toggle to reveal internal IPC plumbing (lifecycle/pipe/stdin).
|
|
7
|
+
*
|
|
8
|
+
* Pure presentational component — data and callbacks come from the page.
|
|
9
|
+
*
|
|
10
|
+
* @module
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { type JSX } from "react";
|
|
14
|
+
import { GitBranch, Hash, MessagesSquare, RefreshCw } from "lucide-react";
|
|
15
|
+
import type { Session, StreamData, TaskData } from "../../hooks/types.js";
|
|
16
|
+
import { groupStreamsByTask, streamKind, type StreamKind } from "../../utils/streamCoordination.js";
|
|
17
|
+
import { ICON_SM } from "../../utils/iconSize.js";
|
|
18
|
+
import styles from "./CoordinationList.module.scss";
|
|
19
|
+
|
|
20
|
+
/** Human-readable label per stream kind. */
|
|
21
|
+
const KIND_LABEL: Record<StreamKind, string> = {
|
|
22
|
+
chatroom: "Chatroom",
|
|
23
|
+
pipe: "Pipe",
|
|
24
|
+
channel: "Channel",
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
/** Icon per stream kind. */
|
|
28
|
+
function KindIcon({ kind }: { kind: StreamKind }): JSX.Element {
|
|
29
|
+
if (kind === "chatroom") {
|
|
30
|
+
return <MessagesSquare size={ICON_SM} aria-hidden="true" />;
|
|
31
|
+
}
|
|
32
|
+
if (kind === "pipe") {
|
|
33
|
+
return <GitBranch size={ICON_SM} aria-hidden="true" />;
|
|
34
|
+
}
|
|
35
|
+
return <Hash size={ICON_SM} aria-hidden="true" />;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Props for the CoordinationList component. */
|
|
39
|
+
export interface CoordinationListProps {
|
|
40
|
+
/** Streams to display (already filtered server-side by the internals toggle). */
|
|
41
|
+
streams: StreamData[];
|
|
42
|
+
/** All known sessions, used to attribute streams to their owning task. */
|
|
43
|
+
sessions: Session[];
|
|
44
|
+
/** Known tasks (only `id` + `title` are used), to render group headers. */
|
|
45
|
+
tasks: readonly Pick<TaskData, "id" | "title">[];
|
|
46
|
+
/** Whether streams are currently loading. */
|
|
47
|
+
loading: boolean;
|
|
48
|
+
/** True if the most recent load attempt failed. */
|
|
49
|
+
loadError?: boolean;
|
|
50
|
+
/** True after at least one load attempt has completed. */
|
|
51
|
+
loadedOnce?: boolean;
|
|
52
|
+
/** Whether internal IPC plumbing is currently shown. */
|
|
53
|
+
showInternals: boolean;
|
|
54
|
+
/** Called when the "Show internals" toggle changes. */
|
|
55
|
+
onToggleInternals: (value: boolean) => void;
|
|
56
|
+
/** Currently selected stream id (for highlight). */
|
|
57
|
+
selectedStreamId?: string;
|
|
58
|
+
/** Called when a stream row is clicked. */
|
|
59
|
+
onSelectStream: (streamId: string) => void;
|
|
60
|
+
/** Optional refresh callback. */
|
|
61
|
+
onRefresh?: () => void;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Read-only, task-grouped inventory of IPC streams. */
|
|
65
|
+
export function CoordinationList({
|
|
66
|
+
streams,
|
|
67
|
+
sessions,
|
|
68
|
+
tasks,
|
|
69
|
+
loading,
|
|
70
|
+
loadError = false,
|
|
71
|
+
loadedOnce = true,
|
|
72
|
+
showInternals,
|
|
73
|
+
onToggleInternals,
|
|
74
|
+
selectedStreamId,
|
|
75
|
+
onSelectStream,
|
|
76
|
+
onRefresh,
|
|
77
|
+
}: CoordinationListProps): JSX.Element {
|
|
78
|
+
const groups = groupStreamsByTask(streams, sessions);
|
|
79
|
+
const kindClass: Record<StreamKind, string> = {
|
|
80
|
+
chatroom: styles.kindChatroom,
|
|
81
|
+
pipe: styles.kindPipe,
|
|
82
|
+
channel: styles.kindChannel,
|
|
83
|
+
};
|
|
84
|
+
const taskTitle = (taskId: string): string => tasks.find((t) => t.id === taskId)?.title ?? taskId;
|
|
85
|
+
|
|
86
|
+
return (
|
|
87
|
+
<div className={styles.container} data-testid="coordination-list">
|
|
88
|
+
<div className={styles.header}>
|
|
89
|
+
<span className={styles.title}>Coordination</span>
|
|
90
|
+
<div className={styles.headerActions}>
|
|
91
|
+
<label className={styles.internalsToggle}>
|
|
92
|
+
<input
|
|
93
|
+
type="checkbox"
|
|
94
|
+
checked={showInternals}
|
|
95
|
+
onChange={(e) => onToggleInternals(e.target.checked)}
|
|
96
|
+
data-testid="coordination-show-internals"
|
|
97
|
+
/>
|
|
98
|
+
Show internals
|
|
99
|
+
</label>
|
|
100
|
+
{onRefresh && (
|
|
101
|
+
<button
|
|
102
|
+
type="button"
|
|
103
|
+
className={styles.refreshButton}
|
|
104
|
+
onClick={onRefresh}
|
|
105
|
+
aria-label="Refresh streams"
|
|
106
|
+
data-testid="coordination-refresh"
|
|
107
|
+
>
|
|
108
|
+
<RefreshCw size={ICON_SM} aria-hidden="true" />
|
|
109
|
+
</button>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
</div>
|
|
113
|
+
|
|
114
|
+
{loading && streams.length === 0 && <div className={styles.state}>Loading{"…"}</div>}
|
|
115
|
+
{!loading && loadError && (
|
|
116
|
+
<div className={styles.state} data-testid="coordination-error">Unable to load streams</div>
|
|
117
|
+
)}
|
|
118
|
+
{!loading && !loadError && loadedOnce && streams.length === 0 && (
|
|
119
|
+
<div className={styles.state} data-testid="coordination-empty">No active streams</div>
|
|
120
|
+
)}
|
|
121
|
+
|
|
122
|
+
{groups.map((group) => (
|
|
123
|
+
<div key={group.taskId ?? "__orphans__"} className={styles.group}>
|
|
124
|
+
<div className={styles.groupHeader}>
|
|
125
|
+
{group.taskId ? taskTitle(group.taskId) : "Unattached / external (CLI · MCP)"}
|
|
126
|
+
</div>
|
|
127
|
+
{group.streams.map((stream) => {
|
|
128
|
+
const kind = streamKind(stream);
|
|
129
|
+
const isSelected = stream.id === selectedStreamId;
|
|
130
|
+
return (
|
|
131
|
+
<button
|
|
132
|
+
key={stream.id}
|
|
133
|
+
type="button"
|
|
134
|
+
className={`${styles.row}${isSelected ? ` ${styles.selected}` : ""}`}
|
|
135
|
+
onClick={() => onSelectStream(stream.id)}
|
|
136
|
+
data-testid={`coordination-row-${stream.id}`}
|
|
137
|
+
aria-current={isSelected ? "page" : undefined}
|
|
138
|
+
>
|
|
139
|
+
<span className={`${styles.kindBadge} ${kindClass[kind]}`}>
|
|
140
|
+
<KindIcon kind={kind} /> {KIND_LABEL[kind]}
|
|
141
|
+
</span>
|
|
142
|
+
<span className={styles.streamName}>{stream.name}</span>
|
|
143
|
+
<span className={styles.meta}>
|
|
144
|
+
{stream.subscriberCount} {stream.subscriberCount === 1 ? "sub" : "subs"} {"·"} {stream.messageBufferDepth} buffered
|
|
145
|
+
</span>
|
|
146
|
+
</button>
|
|
147
|
+
);
|
|
148
|
+
})}
|
|
149
|
+
</div>
|
|
150
|
+
))}
|
|
151
|
+
</div>
|
|
152
|
+
);
|
|
153
|
+
}
|
|
@@ -204,3 +204,33 @@
|
|
|
204
204
|
color: var(--text-disabled, #666);
|
|
205
205
|
font-style: italic;
|
|
206
206
|
}
|
|
207
|
+
|
|
208
|
+
.metaValueMono {
|
|
209
|
+
composes: metaValue;
|
|
210
|
+
word-break: break-all;
|
|
211
|
+
white-space: normal;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.placeholder {
|
|
215
|
+
font-size: 13px;
|
|
216
|
+
color: var(--text-disabled, #666);
|
|
217
|
+
font-style: italic;
|
|
218
|
+
padding: 10px 12px;
|
|
219
|
+
border: 1px dashed var(--border-default, #333);
|
|
220
|
+
border-radius: 6px;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.advanced {
|
|
224
|
+
margin-top: 8px;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.advancedSummary {
|
|
228
|
+
font-size: 11px;
|
|
229
|
+
font-weight: 600;
|
|
230
|
+
text-transform: uppercase;
|
|
231
|
+
letter-spacing: 0.05em;
|
|
232
|
+
color: var(--text-disabled, #666);
|
|
233
|
+
cursor: pointer;
|
|
234
|
+
margin-bottom: 8px;
|
|
235
|
+
user-select: none;
|
|
236
|
+
}
|
|
@@ -13,6 +13,7 @@ const streamWithSubscribers: StreamData = {
|
|
|
13
13
|
name: "agent-chat",
|
|
14
14
|
subscriberCount: 2,
|
|
15
15
|
messageBufferDepth: 5,
|
|
16
|
+
selfEcho: true,
|
|
16
17
|
subscribers: [
|
|
17
18
|
{
|
|
18
19
|
subscriptionId: "sub-001",
|
|
@@ -38,6 +39,7 @@ const streamNoSubscribers: StreamData = {
|
|
|
38
39
|
name: "telemetry-feed",
|
|
39
40
|
subscriberCount: 0,
|
|
40
41
|
messageBufferDepth: 0,
|
|
42
|
+
selfEcho: false,
|
|
41
43
|
subscribers: [],
|
|
42
44
|
};
|
|
43
45
|
|
|
@@ -46,6 +48,7 @@ const streamAllModes: StreamData = {
|
|
|
46
48
|
name: "mixed-modes",
|
|
47
49
|
subscriberCount: 3,
|
|
48
50
|
messageBufferDepth: 0,
|
|
51
|
+
selfEcho: false,
|
|
49
52
|
subscribers: [
|
|
50
53
|
{
|
|
51
54
|
subscriptionId: "sub-rw-async",
|