@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
|
@@ -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",
|
|
@@ -4,12 +4,17 @@
|
|
|
4
4
|
* Renders as an absolutely-positioned overlay anchored to the right of its
|
|
5
5
|
* containing block (which must have `position: relative`).
|
|
6
6
|
*
|
|
7
|
+
* Read-only: participants link to their sessions; low-level wiring (fds, full
|
|
8
|
+
* GUIDs, permission/delivery mode) is tucked behind an "Advanced" disclosure.
|
|
9
|
+
* Live conversation content arrives in V2 (#1230).
|
|
10
|
+
*
|
|
7
11
|
* @module
|
|
8
12
|
*/
|
|
9
13
|
|
|
10
14
|
import { useEffect, type JSX } from "react";
|
|
11
15
|
import type { StreamData } from "../../hooks/types.js";
|
|
12
16
|
import { useAppNavigate, sessionUrl } from "../../utils/navigation.js";
|
|
17
|
+
import { streamKind, type StreamKind } from "../../utils/streamCoordination.js";
|
|
13
18
|
import styles from "./StreamDetailPanel.module.scss";
|
|
14
19
|
|
|
15
20
|
/** Props for the StreamDetailPanel component. */
|
|
@@ -20,6 +25,13 @@ export interface StreamDetailPanelProps {
|
|
|
20
25
|
onClose: () => void;
|
|
21
26
|
}
|
|
22
27
|
|
|
28
|
+
/** Human-readable kind label. */
|
|
29
|
+
const KIND_LABEL: Record<StreamKind, string> = {
|
|
30
|
+
chatroom: "Chatroom",
|
|
31
|
+
pipe: "Pipe",
|
|
32
|
+
channel: "Channel",
|
|
33
|
+
};
|
|
34
|
+
|
|
23
35
|
/** Render a permission badge with appropriate color. */
|
|
24
36
|
function PermissionBadge({ permission }: { permission: string }): JSX.Element {
|
|
25
37
|
const cls = permission === "rw"
|
|
@@ -41,7 +53,8 @@ function DeliveryModeBadge({ mode }: { mode: string }): JSX.Element {
|
|
|
41
53
|
}
|
|
42
54
|
|
|
43
55
|
/**
|
|
44
|
-
* Pull-out right drawer showing stream metadata: overview,
|
|
56
|
+
* Pull-out right drawer showing stream metadata: overview, participants, and an
|
|
57
|
+
* Advanced disclosure with low-level wiring. Conversation content is V2.
|
|
45
58
|
*/
|
|
46
59
|
export function StreamDetailPanel({ stream, onClose }: StreamDetailPanelProps): JSX.Element {
|
|
47
60
|
const navigate = useAppNavigate();
|
|
@@ -61,7 +74,7 @@ export function StreamDetailPanel({ stream, onClose }: StreamDetailPanelProps):
|
|
|
61
74
|
<div className={styles.panel} data-testid="stream-detail-panel">
|
|
62
75
|
<div className={styles.header}>
|
|
63
76
|
<h3 className={styles.title}>{stream.name}</h3>
|
|
64
|
-
<button className={styles.closeButton} onClick={onClose} aria-label="Close stream details">
|
|
77
|
+
<button type="button" className={styles.closeButton} onClick={onClose} aria-label="Close stream details">
|
|
65
78
|
×
|
|
66
79
|
</button>
|
|
67
80
|
</div>
|
|
@@ -71,11 +84,11 @@ export function StreamDetailPanel({ stream, onClose }: StreamDetailPanelProps):
|
|
|
71
84
|
<div className={styles.section}>
|
|
72
85
|
<div className={styles.sectionLabel}>Overview</div>
|
|
73
86
|
<div className={styles.metaRow}>
|
|
74
|
-
<span className={styles.metaKey}>
|
|
75
|
-
<span className={styles.metaValue}>{stream
|
|
87
|
+
<span className={styles.metaKey}>Kind</span>
|
|
88
|
+
<span className={styles.metaValue}>{KIND_LABEL[streamKind(stream)]}</span>
|
|
76
89
|
</div>
|
|
77
90
|
<div className={styles.metaRow}>
|
|
78
|
-
<span className={styles.metaKey}>
|
|
91
|
+
<span className={styles.metaKey}>Participants</span>
|
|
79
92
|
<span className={styles.metaValue}>{stream.subscriberCount}</span>
|
|
80
93
|
</div>
|
|
81
94
|
<div className={styles.metaRow}>
|
|
@@ -84,35 +97,56 @@ export function StreamDetailPanel({ stream, onClose }: StreamDetailPanelProps):
|
|
|
84
97
|
</div>
|
|
85
98
|
</div>
|
|
86
99
|
|
|
87
|
-
{/*
|
|
100
|
+
{/* Participants */}
|
|
88
101
|
<div className={styles.section}>
|
|
89
|
-
<div className={styles.sectionLabel}>
|
|
102
|
+
<div className={styles.sectionLabel}>Participants</div>
|
|
90
103
|
{stream.subscribers.length === 0 ? (
|
|
91
104
|
<div className={styles.emptySubscribers}>No active subscribers</div>
|
|
92
105
|
) : (
|
|
93
106
|
stream.subscribers.map((sub) => (
|
|
94
107
|
<div key={sub.subscriptionId} className={styles.subscriberCard} data-testid={`subscriber-card-${sub.subscriptionId}`}>
|
|
95
|
-
<
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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>
|
|
108
|
+
<button
|
|
109
|
+
type="button"
|
|
110
|
+
className={styles.sessionLink}
|
|
111
|
+
onClick={() => { navigate(sessionUrl(sub.sessionId)); }}
|
|
112
|
+
title={sub.sessionId}
|
|
113
|
+
>
|
|
114
|
+
{sub.sessionId.slice(0, 12)}…
|
|
115
|
+
</button>
|
|
116
|
+
{sub.createdBySpawn && <span className={styles.spawnTag}>spawn</span>}
|
|
112
117
|
</div>
|
|
113
118
|
))
|
|
114
119
|
)}
|
|
115
120
|
</div>
|
|
121
|
+
|
|
122
|
+
{/* Live conversation — V2 */}
|
|
123
|
+
<div className={styles.section}>
|
|
124
|
+
<div className={styles.sectionLabel}>Conversation</div>
|
|
125
|
+
<div className={styles.placeholder} data-testid="stream-conversation-placeholder">
|
|
126
|
+
Live conversation view — coming in V2.
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
|
|
130
|
+
{/* Advanced wiring */}
|
|
131
|
+
<details className={styles.advanced} data-testid="stream-advanced">
|
|
132
|
+
<summary className={styles.advancedSummary}>Advanced</summary>
|
|
133
|
+
<div className={styles.metaRow}>
|
|
134
|
+
<span className={styles.metaKey}>Stream ID</span>
|
|
135
|
+
<span className={styles.metaValueMono}>{stream.id}</span>
|
|
136
|
+
</div>
|
|
137
|
+
{stream.subscribers.map((sub) => (
|
|
138
|
+
<div key={sub.subscriptionId} className={styles.subscriberCard}>
|
|
139
|
+
<div className={styles.subscriberHeader}>
|
|
140
|
+
<span className={styles.fdNumber}>fd {sub.fd}</span>
|
|
141
|
+
<span className={styles.metaValueMono} title={sub.subscriptionId}>{sub.subscriptionId.slice(0, 12)}…</span>
|
|
142
|
+
</div>
|
|
143
|
+
<div className={styles.badges}>
|
|
144
|
+
<PermissionBadge permission={sub.permission} />
|
|
145
|
+
<DeliveryModeBadge mode={sub.deliveryMode} />
|
|
146
|
+
</div>
|
|
147
|
+
</div>
|
|
148
|
+
))}
|
|
149
|
+
</details>
|
|
116
150
|
</div>
|
|
117
151
|
</div>
|
|
118
152
|
);
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* IPC stream inventory and detail panel components (Coordination tab).
|
|
3
3
|
*
|
|
4
4
|
* @module streams
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
export {
|
|
8
|
-
export type {
|
|
7
|
+
export { CoordinationList } from "./CoordinationList.js";
|
|
8
|
+
export type { CoordinationListProps } from "./CoordinationList.js";
|
|
9
9
|
export { StreamDetailPanel } from "./StreamDetailPanel.js";
|
|
10
10
|
export type { StreamDetailPanelProps } from "./StreamDetailPanel.js";
|
package/src/hooks/types.ts
CHANGED
|
@@ -49,6 +49,8 @@ export interface Session {
|
|
|
49
49
|
error?: string;
|
|
50
50
|
endReason?: string;
|
|
51
51
|
personaId?: string;
|
|
52
|
+
/** ID of the task this session belongs to, if any (root/orchestrated work). */
|
|
53
|
+
taskId?: string;
|
|
52
54
|
inputTokens?: number;
|
|
53
55
|
outputTokens?: number;
|
|
54
56
|
costMillicents?: number;
|
|
@@ -641,6 +643,8 @@ export interface StreamData {
|
|
|
641
643
|
messageBufferDepth: number;
|
|
642
644
|
/** Full subscriber details. */
|
|
643
645
|
subscribers: StreamSubscriberData[];
|
|
646
|
+
/** Whether publishers receive their own messages echoed back (marks a chatroom). */
|
|
647
|
+
selfEcho: boolean;
|
|
644
648
|
}
|
|
645
649
|
|
|
646
650
|
/** Values returned by the streams domain hook. */
|
|
@@ -653,8 +657,11 @@ export interface UseStreamsResult {
|
|
|
653
657
|
streamsLoadedOnce: boolean;
|
|
654
658
|
/** True if the most recent loadStreams call failed (e.g. RPC/network error). */
|
|
655
659
|
streamsLoadError: boolean;
|
|
656
|
-
/**
|
|
657
|
-
|
|
660
|
+
/**
|
|
661
|
+
* Request the current stream list from the server. Pass `includeInternal`
|
|
662
|
+
* to surface internal IPC plumbing (lifecycle/pipe/stdin); defaults to false.
|
|
663
|
+
*/
|
|
664
|
+
loadStreams: (includeInternal?: boolean) => Promise<void>;
|
|
658
665
|
/** Handle a domain event from the event bus. Returns `true` if handled. */
|
|
659
666
|
handleEvent: (event: GrackleEvent) => boolean;
|
|
660
667
|
/** Lifecycle hook for connect/disconnect/event routing. */
|
package/src/index.ts
CHANGED
|
@@ -86,9 +86,9 @@ export type { ScheduleManagerProps } from "./components/schedules/ScheduleManage
|
|
|
86
86
|
// Settings
|
|
87
87
|
export { SettingsNav } from "./components/settings/SettingsNav.js";
|
|
88
88
|
|
|
89
|
-
// Streams
|
|
90
|
-
export {
|
|
91
|
-
export type {
|
|
89
|
+
// Streams (Coordination tab)
|
|
90
|
+
export { CoordinationList, StreamDetailPanel } from "./components/streams/index.js";
|
|
91
|
+
export type { CoordinationListProps, StreamDetailPanelProps } from "./components/streams/index.js";
|
|
92
92
|
|
|
93
93
|
// Tools
|
|
94
94
|
export { ToolCard } from "./components/tools/ToolCard.js";
|
|
@@ -165,7 +165,7 @@ export {
|
|
|
165
165
|
SETTINGS_APPEARANCE_URL, SETTINGS_ABOUT_URL, SETTINGS_SHORTCUTS_URL,
|
|
166
166
|
PAIR_PATH, NEW_WORKSPACE_URL, KNOWLEDGE_URL, HOME_URL,
|
|
167
167
|
FINDINGS_URL, findingsUrl, findingUrl,
|
|
168
|
-
CHAT_URL,
|
|
168
|
+
CHAT_URL, COORDINATION_URL, TASKS_URL,
|
|
169
169
|
} from "./utils/navigation.js";
|
|
170
170
|
|
|
171
171
|
export {
|
|
@@ -44,6 +44,7 @@ const NOOP_DOMAIN_HOOK: DomainHook = {
|
|
|
44
44
|
import {
|
|
45
45
|
MOCK_ENVIRONMENTS,
|
|
46
46
|
MOCK_SESSIONS,
|
|
47
|
+
MOCK_STREAMS,
|
|
47
48
|
MOCK_EVENTS,
|
|
48
49
|
MOCK_WORKSPACES,
|
|
49
50
|
MOCK_TASKS,
|
|
@@ -57,7 +58,16 @@ import {
|
|
|
57
58
|
MOCK_KNOWLEDGE_DETAILS,
|
|
58
59
|
type MockStreamStep,
|
|
59
60
|
} from "./mockData.js";
|
|
60
|
-
import type { GraphNode, GraphLink, NodeDetail } from "../hooks/types.js";
|
|
61
|
+
import type { GraphNode, GraphLink, NodeDetail, StreamData } from "../hooks/types.js";
|
|
62
|
+
import { INTERNAL_STREAM_PREFIXES } from "../utils/streamCoordination.js";
|
|
63
|
+
|
|
64
|
+
/** Filter internal IPC plumbing streams unless explicitly requested. */
|
|
65
|
+
function filterMockStreams(includeInternal: boolean): StreamData[] {
|
|
66
|
+
if (includeInternal) {
|
|
67
|
+
return MOCK_STREAMS;
|
|
68
|
+
}
|
|
69
|
+
return MOCK_STREAMS.filter((s) => !INTERNAL_STREAM_PREFIXES.some((p) => s.name.startsWith(p)));
|
|
70
|
+
}
|
|
61
71
|
|
|
62
72
|
// ─── Constants ──────────────────────────────────────
|
|
63
73
|
|
|
@@ -81,6 +91,7 @@ export function MockGrackleProvider({ children }: MockGrackleProviderProps): JSX
|
|
|
81
91
|
// ── State ─────────────────────────────────────────
|
|
82
92
|
const [environments, setEnvironments] = useState<Environment[]>(MOCK_ENVIRONMENTS);
|
|
83
93
|
const [sessions, setSessions] = useState<Session[]>(MOCK_SESSIONS);
|
|
94
|
+
const [streams, setStreams] = useState<StreamData[]>(() => filterMockStreams(false));
|
|
84
95
|
const [events, setEvents] = useState<SessionEvent[]>(MOCK_EVENTS);
|
|
85
96
|
const [lastSpawnedId, setLastSpawnedId] = useState<string | undefined>(undefined);
|
|
86
97
|
const [workspaces, setWorkspaces] = useState<Workspace[]>(MOCK_WORKSPACES);
|
|
@@ -1176,11 +1187,11 @@ export function MockGrackleProvider({ children }: MockGrackleProviderProps): JSX
|
|
|
1176
1187
|
},
|
|
1177
1188
|
|
|
1178
1189
|
streams: {
|
|
1179
|
-
streams
|
|
1190
|
+
streams,
|
|
1180
1191
|
streamsLoading: false,
|
|
1181
1192
|
streamsLoadedOnce: true,
|
|
1182
1193
|
streamsLoadError: false,
|
|
1183
|
-
loadStreams: async () => {},
|
|
1194
|
+
loadStreams: async (includeInternal = false) => { setStreams(filterMockStreams(includeInternal)); },
|
|
1184
1195
|
domainHook: NOOP_DOMAIN_HOOK,
|
|
1185
1196
|
},
|
|
1186
1197
|
|
|
@@ -1371,6 +1382,7 @@ export function MockGrackleProvider({ children }: MockGrackleProviderProps): JSX
|
|
|
1371
1382
|
[
|
|
1372
1383
|
environments,
|
|
1373
1384
|
sessions,
|
|
1385
|
+
streams,
|
|
1374
1386
|
events,
|
|
1375
1387
|
lastSpawnedId,
|
|
1376
1388
|
taskSessions,
|
package/src/mocks/mockData.ts
CHANGED
|
@@ -17,6 +17,7 @@ import type {
|
|
|
17
17
|
PersonaData,
|
|
18
18
|
} from "../hooks/types.js";
|
|
19
19
|
export { MOCK_KNOWLEDGE_NODES, MOCK_KNOWLEDGE_LINKS, MOCK_KNOWLEDGE_DETAILS } from "./mockKnowledgeData.js";
|
|
20
|
+
export { MOCK_STREAMS } from "./mockStreamsData.js";
|
|
20
21
|
|
|
21
22
|
// ─── Environments ───────────────────────────────────
|
|
22
23
|
|
|
@@ -94,6 +95,7 @@ export const MOCK_SESSIONS: Session[] = [
|
|
|
94
95
|
status: "running",
|
|
95
96
|
prompt: "Refactor the authentication middleware to use JWT tokens",
|
|
96
97
|
startedAt: "2026-02-27T08:15:00Z",
|
|
98
|
+
taskId: "task-001",
|
|
97
99
|
inputTokens: 42_600,
|
|
98
100
|
outputTokens: 8_100,
|
|
99
101
|
costMillicents: 22_000,
|
|
@@ -107,6 +109,7 @@ export const MOCK_SESSIONS: Session[] = [
|
|
|
107
109
|
prompt: "Write unit tests for the user registration endpoint",
|
|
108
110
|
startedAt: "2026-02-27T07:30:00Z",
|
|
109
111
|
endedAt: "2026-02-27T07:33:00Z",
|
|
112
|
+
taskId: "task-003",
|
|
110
113
|
inputTokens: 31_400,
|
|
111
114
|
outputTokens: 9_800,
|
|
112
115
|
costMillicents: 18_000,
|
|
@@ -131,6 +134,7 @@ export const MOCK_SESSIONS: Session[] = [
|
|
|
131
134
|
status: "running",
|
|
132
135
|
prompt: "Implement rate limiting for the public API",
|
|
133
136
|
startedAt: "2026-02-27T09:00:00Z",
|
|
137
|
+
taskId: "task-006c",
|
|
134
138
|
inputTokens: 18_900,
|
|
135
139
|
outputTokens: 4_500,
|
|
136
140
|
costMillicents: 10_000,
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Static mock IPC streams for the Coordination surface in `?mock` demo mode.
|
|
3
|
+
*
|
|
4
|
+
* Subscribers reference sessions from {@link MOCK_SESSIONS}; sessions carry a
|
|
5
|
+
* `taskId`, so the Coordination tab attributes each stream to its owning task.
|
|
6
|
+
* Internal plumbing (`lifecycle:`/`pipe:`/`stdin:`) is hidden until the user
|
|
7
|
+
* toggles "Show internals".
|
|
8
|
+
*
|
|
9
|
+
* @module
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { StreamData } from "../hooks/types.js";
|
|
13
|
+
|
|
14
|
+
/** Sample IPC streams: a chatroom + channel attributed to tasks, an unattached
|
|
15
|
+
* stream, and internal plumbing streams. */
|
|
16
|
+
export const MOCK_STREAMS: StreamData[] = [
|
|
17
|
+
{
|
|
18
|
+
id: "stream-planning",
|
|
19
|
+
name: "jwt-planning-room",
|
|
20
|
+
selfEcho: true,
|
|
21
|
+
subscriberCount: 2,
|
|
22
|
+
messageBufferDepth: 3,
|
|
23
|
+
subscribers: [
|
|
24
|
+
{ subscriptionId: "sub-p1", sessionId: "sess-001", fd: 3, permission: "rw", deliveryMode: "async", createdBySpawn: false },
|
|
25
|
+
{ subscriptionId: "sub-p2", sessionId: "sess-002", fd: 4, permission: "r", deliveryMode: "async", createdBySpawn: true },
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "stream-metrics",
|
|
30
|
+
name: "rate-limit-metrics",
|
|
31
|
+
selfEcho: false,
|
|
32
|
+
subscriberCount: 1,
|
|
33
|
+
messageBufferDepth: 0,
|
|
34
|
+
subscribers: [
|
|
35
|
+
{ subscriptionId: "sub-m1", sessionId: "sess-004", fd: 3, permission: "r", deliveryMode: "sync", createdBySpawn: false },
|
|
36
|
+
],
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: "stream-cli",
|
|
40
|
+
name: "cli-inspector",
|
|
41
|
+
selfEcho: false,
|
|
42
|
+
subscriberCount: 1,
|
|
43
|
+
messageBufferDepth: 1,
|
|
44
|
+
subscribers: [
|
|
45
|
+
{ subscriptionId: "sub-c1", sessionId: "external-cli-session", fd: 5, permission: "rw", deliveryMode: "async", createdBySpawn: false },
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
// ── Internal plumbing — hidden unless "Show internals" is on ──
|
|
49
|
+
{
|
|
50
|
+
id: "stream-lifecycle",
|
|
51
|
+
name: "lifecycle:sess-001-7f3a",
|
|
52
|
+
selfEcho: false,
|
|
53
|
+
subscriberCount: 1,
|
|
54
|
+
messageBufferDepth: 0,
|
|
55
|
+
subscribers: [
|
|
56
|
+
{ subscriptionId: "sub-l1", sessionId: "sess-001", fd: 6, permission: "rw", deliveryMode: "detach", createdBySpawn: true },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: "stream-pipe",
|
|
61
|
+
name: "pipe:sess-001-sess-004",
|
|
62
|
+
selfEcho: false,
|
|
63
|
+
subscriberCount: 2,
|
|
64
|
+
messageBufferDepth: 0,
|
|
65
|
+
subscribers: [
|
|
66
|
+
{ subscriptionId: "sub-pp1", sessionId: "sess-001", fd: 7, permission: "rw", deliveryMode: "async", createdBySpawn: false },
|
|
67
|
+
{ subscriptionId: "sub-pp2", sessionId: "sess-004", fd: 8, permission: "rw", deliveryMode: "async", createdBySpawn: true },
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
id: "stream-stdin",
|
|
72
|
+
name: "stdin:sess-002-9c2e",
|
|
73
|
+
selfEcho: false,
|
|
74
|
+
subscriberCount: 1,
|
|
75
|
+
messageBufferDepth: 0,
|
|
76
|
+
subscribers: [
|
|
77
|
+
{ subscriptionId: "sub-s1", sessionId: "sess-002", fd: 9, permission: "w", deliveryMode: "detach", createdBySpawn: true },
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
];
|
package/src/utils/navigation.ts
CHANGED
|
@@ -159,13 +159,11 @@ export const SETTINGS_SHORTCUTS_URL: string = "/settings/shortcuts";
|
|
|
159
159
|
/** URL for the device pairing page. */
|
|
160
160
|
export const PAIR_PATH: string = "/pair";
|
|
161
161
|
|
|
162
|
-
/** URL for the root-task chat page. */
|
|
162
|
+
/** URL for the root-task ("Root") chat page. */
|
|
163
163
|
export const CHAT_URL: string = "/chat";
|
|
164
164
|
|
|
165
|
-
/**
|
|
166
|
-
export
|
|
167
|
-
return `/chat/${encodeURIComponent(streamId)}`;
|
|
168
|
-
}
|
|
165
|
+
/** URL for the Coordination page (read-only IPC stream inventory). */
|
|
166
|
+
export const COORDINATION_URL: string = "/coordination";
|
|
169
167
|
|
|
170
168
|
/** URL for the home dashboard page. */
|
|
171
169
|
export const HOME_URL: string = "/";
|