@grackle-ai/web-components 0.112.2 → 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.
Files changed (48) hide show
  1. package/.rush/temp/{a0341c0f1c835c664217d8a879aa38d780e62122.tar.log → 989dfd9dfc66be6288052dccbf2952ddad9066c6.tar.log} +2 -2
  2. package/.rush/temp/{1421806d07f6b0c455deca4bf89a6412726ffd8b.untar.log → 989dfd9dfc66be6288052dccbf2952ddad9066c6.untar.log} +2 -2
  3. package/.rush/temp/{1421806d07f6b0c455deca4bf89a6412726ffd8b.tar.log → b1a36bcc314d65fbd843fcff6d3127ebdf827e06.tar.log} +81 -81
  4. package/.rush/temp/{a0341c0f1c835c664217d8a879aa38d780e62122.untar.log → b1a36bcc314d65fbd843fcff6d3127ebdf827e06.untar.log} +2 -2
  5. package/.rush/temp/chunked-rush-logs/web-components._phase_build.chunks.jsonl +7 -6
  6. package/.rush/temp/chunked-rush-logs/web-components._phase_test.chunks.jsonl +25 -24
  7. package/.rush/temp/operation/_phase_build/all.log +7 -6
  8. package/.rush/temp/operation/_phase_build/log-chunks.jsonl +7 -6
  9. package/.rush/temp/operation/_phase_build/state.json +1 -1
  10. package/.rush/temp/operation/_phase_test/all.log +25 -24
  11. package/.rush/temp/operation/_phase_test/log-chunks.jsonl +25 -24
  12. package/.rush/temp/operation/_phase_test/state.json +1 -1
  13. package/README.md +2 -1
  14. package/dist/McpAppWidget-CSX2W2Vb.js +5774 -0
  15. package/dist/index.css +1 -1
  16. package/dist/index.js +12221 -17764
  17. package/package.json +2 -2
  18. package/rush-logs/web-components._phase_build.cache.log +1 -1
  19. package/rush-logs/web-components._phase_build.log +7 -6
  20. package/rush-logs/web-components._phase_test.cache.log +1 -1
  21. package/rush-logs/web-components._phase_test.log +25 -24
  22. package/src/components/display/EventRenderer.stories.tsx +22 -0
  23. package/src/components/display/EventRenderer.tsx +44 -2
  24. package/src/components/display/EventStream.tsx +4 -1
  25. package/src/components/display/index.ts +3 -1
  26. package/src/components/layout/AppNav.stories.tsx +5 -5
  27. package/src/components/layout/AppNav.tsx +8 -4
  28. package/src/components/panels/KeyboardShortcutsPanel.stories.tsx +1 -1
  29. package/src/components/panels/KeyboardShortcutsPanel.tsx +1 -1
  30. package/src/components/streams/CoordinationList.module.scss +137 -0
  31. package/src/components/streams/CoordinationList.stories.tsx +95 -0
  32. package/src/components/streams/CoordinationList.tsx +153 -0
  33. package/src/components/streams/StreamDetailPanel.module.scss +30 -0
  34. package/src/components/streams/StreamDetailPanel.stories.tsx +3 -0
  35. package/src/components/streams/StreamDetailPanel.tsx +58 -24
  36. package/src/components/streams/index.ts +3 -3
  37. package/src/hooks/types.ts +9 -2
  38. package/src/index.ts +5 -5
  39. package/src/mocks/MockGrackleProvider.tsx +15 -3
  40. package/src/mocks/mockData.ts +4 -0
  41. package/src/mocks/mockStreamsData.ts +80 -0
  42. package/src/utils/navigation.ts +3 -5
  43. package/src/utils/streamCoordination.test.ts +88 -0
  44. package/src/utils/streamCoordination.ts +108 -0
  45. package/temp/build/lint/_eslint-5eVG3S6w.json +42 -30
  46. package/src/components/streams/StreamList.module.scss +0 -92
  47. package/src/components/streams/StreamList.stories.tsx +0 -99
  48. 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, subscribers, fds.
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
  &times;
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}>Stream ID</span>
75
- <span className={styles.metaValue}>{stream.id}</span>
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}>Subscribers</span>
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
- {/* Subscribers */}
100
+ {/* Participants */}
88
101
  <div className={styles.section}>
89
- <div className={styles.sectionLabel}>Subscribers</div>
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
- <div className={styles.subscriberHeader}>
96
- <span className={styles.fdNumber}>fd {sub.fd}</span>
97
- <button
98
- className={styles.sessionLink}
99
- onClick={() => { navigate(sessionUrl(sub.sessionId)); }}
100
- title={sub.sessionId}
101
- >
102
- {sub.sessionId.slice(0, 12)}…
103
- </button>
104
- </div>
105
- <div className={styles.badges}>
106
- <PermissionBadge permission={sub.permission} />
107
- <DeliveryModeBadge mode={sub.deliveryMode} />
108
- {sub.createdBySpawn && (
109
- <span className={styles.spawnTag}>spawn</span>
110
- )}
111
- </div>
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
- * Stream sidebar and detail panel components.
2
+ * IPC stream inventory and detail panel components (Coordination tab).
3
3
  *
4
4
  * @module streams
5
5
  */
6
6
 
7
- export { StreamList } from "./StreamList.js";
8
- export type { StreamListProps } from "./StreamList.js";
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";
@@ -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
- /** Request the current stream list from the server. */
657
- loadStreams: () => Promise<void>;
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
@@ -19,7 +19,7 @@ export { useDagLayout } from "./components/dag/useDagLayout.js";
19
19
  export {
20
20
  Breadcrumbs, Button, CopyButton, DemoBanner, SplitButton,
21
21
  EventRenderer, ConfirmDialog, Skeleton, SkeletonText, SkeletonCard,
22
- Spinner, SplashScreen, Tooltip, McpAppWidget,
22
+ Spinner, SplashScreen, Tooltip,
23
23
  } from "./components/display/index.js";
24
24
  export type { ButtonProps, ButtonVariant, ButtonSize } from "./components/display/index.js";
25
25
  export type { TooltipProps, TooltipPlacement } from "./components/display/index.js";
@@ -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 { StreamList, StreamDetailPanel } from "./components/streams/index.js";
91
- export type { StreamListProps, StreamDetailPanelProps } from "./components/streams/index.js";
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, chatStreamUrl, TASKS_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,
@@ -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
+ ];
@@ -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
- /** Build URL for a specific IPC stream's chat page. */
166
- export function chatStreamUrl(streamId: string): string {
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 = "/";