@grackle-ai/web-components 0.107.2
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/3ae72563f781afd72723475938136f113846603e.untar.log +10 -0
- package/.rush/temp/bc1d5bf9201ce71abeaeaddd096deb9b0805d703.untar.log +10 -0
- package/.rush/temp/operation/_phase_build/all.log +18 -0
- package/.rush/temp/operation/_phase_build/log-chunks.jsonl +18 -0
- package/.rush/temp/operation/_phase_build/state.json +3 -0
- package/.rush/temp/operation/_phase_test/all.log +121 -0
- package/.rush/temp/operation/_phase_test/log-chunks.jsonl +121 -0
- package/.rush/temp/operation/_phase_test/state.json +3 -0
- package/.rush/temp/shrinkwrap-deps.json +938 -0
- package/.storybook/main.ts +22 -0
- package/.storybook/preview.tsx +30 -0
- package/config/rig.json +4 -0
- package/config/rush-project.json +12 -0
- package/dist/index.css +1 -0
- package/dist/index.js +39221 -0
- package/eslint.config.cjs +5 -0
- package/package.json +83 -0
- package/rush-logs/web-components._phase_build.cache.log +4 -0
- package/rush-logs/web-components._phase_test.cache.log +4 -0
- package/src/components/chat/ChatInput.module.scss +81 -0
- package/src/components/chat/ChatInput.stories.tsx +91 -0
- package/src/components/chat/ChatInput.tsx +168 -0
- package/src/components/chat/index.ts +6 -0
- package/src/components/dag/DagView.module.scss +149 -0
- package/src/components/dag/DagView.stories.tsx +125 -0
- package/src/components/dag/DagView.tsx +109 -0
- package/src/components/dag/TaskNode.stories.tsx +133 -0
- package/src/components/dag/TaskNode.tsx +40 -0
- package/src/components/dag/useDagLayout.ts +139 -0
- package/src/components/display/Breadcrumbs.module.scss +71 -0
- package/src/components/display/Breadcrumbs.stories.tsx +80 -0
- package/src/components/display/Breadcrumbs.tsx +46 -0
- package/src/components/display/Button.module.scss +110 -0
- package/src/components/display/Button.stories.tsx +88 -0
- package/src/components/display/Button.tsx +40 -0
- package/src/components/display/ConfirmDialog.module.scss +67 -0
- package/src/components/display/ConfirmDialog.stories.tsx +81 -0
- package/src/components/display/ConfirmDialog.tsx +88 -0
- package/src/components/display/CopyButton.module.scss +41 -0
- package/src/components/display/CopyButton.stories.tsx +78 -0
- package/src/components/display/CopyButton.tsx +64 -0
- package/src/components/display/DemoBanner.module.scss +37 -0
- package/src/components/display/DemoBanner.stories.tsx +40 -0
- package/src/components/display/DemoBanner.tsx +23 -0
- package/src/components/display/EventHoverRow.module.scss +102 -0
- package/src/components/display/EventHoverRow.stories.tsx +99 -0
- package/src/components/display/EventHoverRow.tsx +154 -0
- package/src/components/display/EventRenderer.module.scss +272 -0
- package/src/components/display/EventRenderer.stories.tsx +186 -0
- package/src/components/display/EventRenderer.tsx +271 -0
- package/src/components/display/EventStream.module.scss +93 -0
- package/src/components/display/EventStream.stories.tsx +249 -0
- package/src/components/display/EventStream.tsx +369 -0
- package/src/components/display/FloatingActionBar.module.scss +107 -0
- package/src/components/display/FloatingActionBar.stories.tsx +122 -0
- package/src/components/display/FloatingActionBar.tsx +119 -0
- package/src/components/display/SessionAttemptSelector.module.scss +50 -0
- package/src/components/display/SessionAttemptSelector.stories.tsx +78 -0
- package/src/components/display/SessionAttemptSelector.tsx +49 -0
- package/src/components/display/SessionPicker.module.scss +200 -0
- package/src/components/display/SessionPicker.stories.tsx +169 -0
- package/src/components/display/SessionPicker.tsx +214 -0
- package/src/components/display/Skeleton.module.scss +58 -0
- package/src/components/display/Skeleton.stories.tsx +94 -0
- package/src/components/display/Skeleton.tsx +127 -0
- package/src/components/display/Spinner.module.scss +41 -0
- package/src/components/display/Spinner.stories.tsx +66 -0
- package/src/components/display/Spinner.tsx +32 -0
- package/src/components/display/SplashScreen.module.scss +20 -0
- package/src/components/display/SplashScreen.stories.tsx +26 -0
- package/src/components/display/SplashScreen.tsx +16 -0
- package/src/components/display/SplitButton.module.scss +166 -0
- package/src/components/display/SplitButton.stories.tsx +95 -0
- package/src/components/display/SplitButton.tsx +128 -0
- package/src/components/display/Tooltip.module.scss +84 -0
- package/src/components/display/Tooltip.stories.tsx +240 -0
- package/src/components/display/Tooltip.tsx +184 -0
- package/src/components/display/extractText.test.tsx +48 -0
- package/src/components/display/index.ts +20 -0
- package/src/components/editable/EditableCheckbox.stories.tsx +54 -0
- package/src/components/editable/EditableCheckbox.tsx +39 -0
- package/src/components/editable/EditableField.module.scss +135 -0
- package/src/components/editable/EditableSelect.tsx +164 -0
- package/src/components/editable/EditableTextArea.stories.tsx +50 -0
- package/src/components/editable/EditableTextArea.tsx +148 -0
- package/src/components/editable/EditableTextField.stories.tsx +62 -0
- package/src/components/editable/EditableTextField.tsx +153 -0
- package/src/components/editable/EnvironmentSelect.module.scss +17 -0
- package/src/components/editable/EnvironmentSelect.stories.tsx +61 -0
- package/src/components/editable/EnvironmentSelect.tsx +87 -0
- package/src/components/editable/index.ts +13 -0
- package/src/components/editable/useEditableField.test.tsx +233 -0
- package/src/components/editable/useEditableField.ts +173 -0
- package/src/components/index.ts +20 -0
- package/src/components/knowledge/KnowledgeDetailPanel.module.scss +162 -0
- package/src/components/knowledge/KnowledgeDetailPanel.stories.tsx +208 -0
- package/src/components/knowledge/KnowledgeDetailPanel.tsx +122 -0
- package/src/components/knowledge/KnowledgeGraph.module.scss +110 -0
- package/src/components/knowledge/KnowledgeGraph.stories.tsx +180 -0
- package/src/components/knowledge/KnowledgeGraph.tsx +455 -0
- package/src/components/knowledge/KnowledgeNav.module.scss +130 -0
- package/src/components/knowledge/KnowledgeNav.stories.tsx +108 -0
- package/src/components/knowledge/KnowledgeNav.tsx +138 -0
- package/src/components/knowledge/index.ts +3 -0
- package/src/components/layout/AppNav.module.scss +82 -0
- package/src/components/layout/AppNav.stories.tsx +115 -0
- package/src/components/layout/AppNav.tsx +133 -0
- package/src/components/layout/BottomStatusBar.module.scss +58 -0
- package/src/components/layout/BottomStatusBar.stories.tsx +35 -0
- package/src/components/layout/BottomStatusBar.tsx +206 -0
- package/src/components/layout/Sidebar.module.scss +60 -0
- package/src/components/layout/Sidebar.stories.tsx +46 -0
- package/src/components/layout/Sidebar.tsx +84 -0
- package/src/components/layout/StatusBar.module.scss +108 -0
- package/src/components/layout/StatusBar.stories.tsx +119 -0
- package/src/components/layout/StatusBar.tsx +70 -0
- package/src/components/layout/index.ts +9 -0
- package/src/components/lists/EnvironmentNav.module.scss +118 -0
- package/src/components/lists/EnvironmentNav.stories.tsx +121 -0
- package/src/components/lists/EnvironmentNav.tsx +133 -0
- package/src/components/lists/FindingsNav.module.scss +126 -0
- package/src/components/lists/FindingsNav.tsx +146 -0
- package/src/components/lists/TaskList.module.scss +206 -0
- package/src/components/lists/TaskList.stories.tsx +401 -0
- package/src/components/lists/TaskList.tsx +509 -0
- package/src/components/lists/index.ts +6 -0
- package/src/components/lists/listHelpers.tsx +130 -0
- package/src/components/notifications/Callout.module.scss +83 -0
- package/src/components/notifications/Callout.stories.tsx +81 -0
- package/src/components/notifications/Callout.tsx +64 -0
- package/src/components/notifications/Toast.module.scss +86 -0
- package/src/components/notifications/Toast.stories.tsx +71 -0
- package/src/components/notifications/Toast.tsx +51 -0
- package/src/components/notifications/ToastContainer.module.scss +23 -0
- package/src/components/notifications/ToastContainer.stories.tsx +66 -0
- package/src/components/notifications/ToastContainer.tsx +29 -0
- package/src/components/notifications/UpdateBanner.stories.tsx +77 -0
- package/src/components/notifications/UpdateBanner.test.tsx +64 -0
- package/src/components/notifications/UpdateBanner.tsx +44 -0
- package/src/components/notifications/index.ts +8 -0
- package/src/components/panels/AboutPanel.stories.tsx +70 -0
- package/src/components/panels/AboutPanel.tsx +66 -0
- package/src/components/panels/AppearancePanel.stories.tsx +45 -0
- package/src/components/panels/AppearancePanel.tsx +97 -0
- package/src/components/panels/CredentialProvidersPanel.stories.tsx +62 -0
- package/src/components/panels/CredentialProvidersPanel.tsx +111 -0
- package/src/components/panels/EnvironmentEditPanel.module.scss +170 -0
- package/src/components/panels/EnvironmentEditPanel.stories.tsx +206 -0
- package/src/components/panels/EnvironmentEditPanel.tsx +785 -0
- package/src/components/panels/FindingsPanel.module.scss +94 -0
- package/src/components/panels/FindingsPanel.stories.tsx +109 -0
- package/src/components/panels/FindingsPanel.tsx +76 -0
- package/src/components/panels/KeyboardShortcutsPanel.module.scss +65 -0
- package/src/components/panels/KeyboardShortcutsPanel.stories.tsx +40 -0
- package/src/components/panels/KeyboardShortcutsPanel.tsx +104 -0
- package/src/components/panels/PluginsPanel.tsx +77 -0
- package/src/components/panels/SettingsPanel.module.scss +336 -0
- package/src/components/panels/TaskActionButtons.module.scss +22 -0
- package/src/components/panels/TaskActionButtons.stories.tsx +125 -0
- package/src/components/panels/TaskActionButtons.tsx +87 -0
- package/src/components/panels/TaskEditPanel.module.scss +202 -0
- package/src/components/panels/TaskEditPanel.stories.tsx +75 -0
- package/src/components/panels/TaskEditPanel.tsx +328 -0
- package/src/components/panels/TaskOverviewPanel.module.scss +236 -0
- package/src/components/panels/TaskOverviewPanel.stories.tsx +219 -0
- package/src/components/panels/TaskOverviewPanel.tsx +270 -0
- package/src/components/panels/TokensPanel.stories.tsx +131 -0
- package/src/components/panels/TokensPanel.tsx +143 -0
- package/src/components/panels/WorkpadPanel.module.scss +39 -0
- package/src/components/panels/WorkpadPanel.stories.tsx +56 -0
- package/src/components/panels/WorkpadPanel.tsx +63 -0
- package/src/components/panels/index.ts +13 -0
- package/src/components/personas/McpToolSelector.module.scss +109 -0
- package/src/components/personas/McpToolSelector.stories.tsx +129 -0
- package/src/components/personas/McpToolSelector.tsx +180 -0
- package/src/components/personas/PersonaManager.module.scss +233 -0
- package/src/components/personas/PersonaManager.stories.tsx +139 -0
- package/src/components/personas/PersonaManager.tsx +122 -0
- package/src/components/schedules/ScheduleManager.module.scss +98 -0
- package/src/components/schedules/ScheduleManager.stories.tsx +78 -0
- package/src/components/schedules/ScheduleManager.tsx +160 -0
- package/src/components/settings/SettingsNav.module.scss +82 -0
- package/src/components/settings/SettingsNav.stories.tsx +83 -0
- package/src/components/settings/SettingsNav.tsx +104 -0
- package/src/components/streams/StreamDetailPanel.module.scss +206 -0
- package/src/components/streams/StreamDetailPanel.stories.tsx +132 -0
- package/src/components/streams/StreamDetailPanel.tsx +119 -0
- package/src/components/streams/StreamList.module.scss +92 -0
- package/src/components/streams/StreamList.stories.tsx +99 -0
- package/src/components/streams/StreamList.tsx +114 -0
- package/src/components/streams/index.ts +10 -0
- package/src/components/tools/AgentToolCard.module.scss +118 -0
- package/src/components/tools/AgentToolCard.stories.tsx +304 -0
- package/src/components/tools/AgentToolCard.tsx +247 -0
- package/src/components/tools/FileEditCard.stories.tsx +138 -0
- package/src/components/tools/FileEditCard.tsx +160 -0
- package/src/components/tools/FileReadCard.stories.tsx +120 -0
- package/src/components/tools/FileReadCard.tsx +106 -0
- package/src/components/tools/FindingCard.stories.tsx +124 -0
- package/src/components/tools/FindingCard.tsx +178 -0
- package/src/components/tools/GenericToolCard.stories.tsx +80 -0
- package/src/components/tools/GenericToolCard.tsx +111 -0
- package/src/components/tools/IpcCard.stories.tsx +129 -0
- package/src/components/tools/IpcCard.tsx +178 -0
- package/src/components/tools/KnowledgeCard.stories.tsx +112 -0
- package/src/components/tools/KnowledgeCard.tsx +165 -0
- package/src/components/tools/MetadataCard.stories.tsx +32 -0
- package/src/components/tools/MetadataCard.tsx +39 -0
- package/src/components/tools/SearchCard.stories.tsx +74 -0
- package/src/components/tools/SearchCard.tsx +86 -0
- package/src/components/tools/ShellCard.stories.tsx +112 -0
- package/src/components/tools/ShellCard.tsx +106 -0
- package/src/components/tools/TaskCard.stories.tsx +123 -0
- package/src/components/tools/TaskCard.tsx +203 -0
- package/src/components/tools/TodoCard.module.scss +131 -0
- package/src/components/tools/TodoCard.stories.tsx +202 -0
- package/src/components/tools/TodoCard.tsx +200 -0
- package/src/components/tools/ToolCard.stories.tsx +177 -0
- package/src/components/tools/ToolCard.tsx +60 -0
- package/src/components/tools/ToolCardProps.ts +20 -0
- package/src/components/tools/ToolSearchCard.stories.tsx +81 -0
- package/src/components/tools/ToolSearchCard.tsx +86 -0
- package/src/components/tools/WorkpadCard.stories.tsx +106 -0
- package/src/components/tools/WorkpadCard.tsx +125 -0
- package/src/components/tools/classifyTool.test.ts +44 -0
- package/src/components/tools/classifyTool.ts +134 -0
- package/src/components/tools/parseDiff.ts +95 -0
- package/src/components/tools/parseShellOutput.ts +28 -0
- package/src/components/tools/toolCardHelpers.test.ts +53 -0
- package/src/components/tools/toolCards.module.scss +234 -0
- package/src/components/workspace/WorkspaceBoard.module.scss +238 -0
- package/src/components/workspace/WorkspaceBoard.stories.tsx +240 -0
- package/src/components/workspace/WorkspaceBoard.tsx +232 -0
- package/src/components/workspace/WorkspaceFormFields.module.scss +79 -0
- package/src/components/workspace/WorkspaceFormFields.stories.tsx +133 -0
- package/src/components/workspace/WorkspaceFormFields.tsx +185 -0
- package/src/context/GrackleContext.ts +28 -0
- package/src/context/GrackleContextTypes.ts +64 -0
- package/src/context/SidebarContext.tsx +53 -0
- package/src/context/ThemeContext.tsx +21 -0
- package/src/context/ToastContext.tsx +56 -0
- package/src/hooks/types.ts +864 -0
- package/src/hooks/useEventSelection.test.ts +204 -0
- package/src/hooks/useEventSelection.ts +158 -0
- package/src/hooks/useSmartScroll.ts +151 -0
- package/src/hooks/useTheme.ts +228 -0
- package/src/index.ts +210 -0
- package/src/mocks/MockGrackleProvider.tsx +1397 -0
- package/src/mocks/mockData.ts +1966 -0
- package/src/mocks/mockKnowledgeData.ts +294 -0
- package/src/scss.d.ts +12 -0
- package/src/styles/global.scss +244 -0
- package/src/styles/mixins.scss +278 -0
- package/src/styles/prism-theme.scss +148 -0
- package/src/styles/theme.scss +1102 -0
- package/src/test-utils/storybook-decorators.tsx +50 -0
- package/src/test-utils/storybook-helpers.ts +262 -0
- package/src/themes.ts +142 -0
- package/src/utils/boardColumns.ts +141 -0
- package/src/utils/breadcrumbs.test.ts +285 -0
- package/src/utils/breadcrumbs.ts +222 -0
- package/src/utils/dashboard.test.ts +156 -0
- package/src/utils/dashboard.ts +195 -0
- package/src/utils/eventContent.test.ts +353 -0
- package/src/utils/eventContent.ts +209 -0
- package/src/utils/findingCategory.ts +33 -0
- package/src/utils/format.ts +27 -0
- package/src/utils/iconSize.ts +18 -0
- package/src/utils/navigation.ts +205 -0
- package/src/utils/route-config.test.ts +128 -0
- package/src/utils/scrollUtils.test.ts +65 -0
- package/src/utils/scrollUtils.ts +49 -0
- package/src/utils/sessionEvents.test.ts +302 -0
- package/src/utils/sessionEvents.ts +233 -0
- package/src/utils/taskStatus.tsx +137 -0
- package/src/utils/time.ts +92 -0
- package/tsconfig.json +8 -0
- package/vite.config.ts +20 -0
- package/vitest.config.ts +10 -0
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { type ReactNode, useState, type JSX } from "react";
|
|
2
|
+
import { ChevronDown, ChevronRight } from "lucide-react";
|
|
3
|
+
import Markdown from "react-markdown";
|
|
4
|
+
import rehypePrismPlus from "rehype-prism-plus/common";
|
|
5
|
+
import remarkGfm from "remark-gfm";
|
|
6
|
+
import type { SessionEvent } from "../../hooks/types.js";
|
|
7
|
+
import { formatTokens, formatCost } from "../../utils/format.js";
|
|
8
|
+
import { ICON_SM } from "../../utils/iconSize.js";
|
|
9
|
+
import { ToolCard } from "../tools/ToolCard.js";
|
|
10
|
+
import { CopyButton } from "./CopyButton.js";
|
|
11
|
+
import styles from "./EventRenderer.module.scss";
|
|
12
|
+
|
|
13
|
+
/** Props for the EventRenderer component. */
|
|
14
|
+
interface Props {
|
|
15
|
+
event: SessionEvent;
|
|
16
|
+
/** Paired tool_use context, attached by SessionPanel when raw IDs match. */
|
|
17
|
+
toolUseCtx?: { tool: string; args: unknown; detailedResult?: string };
|
|
18
|
+
/** True when a tool_use completed but has no tool_result (e.g. Claude Code text-result pattern). */
|
|
19
|
+
settled?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// --- Individual event type renderers ---
|
|
23
|
+
|
|
24
|
+
/** Number of lines shown in the collapsed system context preview. */
|
|
25
|
+
const SYSTEM_CONTEXT_PREVIEW_LINES: number = 3;
|
|
26
|
+
|
|
27
|
+
/** Renders the system context (system prompt) as a collapsible left-bordered section. */
|
|
28
|
+
function SystemContextEvent({ content }: { content: string }): JSX.Element {
|
|
29
|
+
const [expanded, setExpanded] = useState(false);
|
|
30
|
+
const lines = content.split("\n");
|
|
31
|
+
const hasMore = lines.length > SYSTEM_CONTEXT_PREVIEW_LINES;
|
|
32
|
+
const displayContent = expanded ? content : lines.slice(0, SYSTEM_CONTEXT_PREVIEW_LINES).join("\n");
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div className={styles.systemContextEvent} data-testid="system-context-event">
|
|
36
|
+
<button
|
|
37
|
+
type="button"
|
|
38
|
+
className={styles.systemContextHeader}
|
|
39
|
+
onClick={() => { setExpanded((v) => !v); }}
|
|
40
|
+
aria-expanded={expanded}
|
|
41
|
+
>
|
|
42
|
+
<span className={styles.systemContextBadge}>SYSTEM PROMPT</span>
|
|
43
|
+
{hasMore && (
|
|
44
|
+
<span className={styles.systemContextToggle} aria-hidden="true">
|
|
45
|
+
{expanded ? <ChevronDown size={ICON_SM} /> : <ChevronRight size={ICON_SM} />}
|
|
46
|
+
</span>
|
|
47
|
+
)}
|
|
48
|
+
</button>
|
|
49
|
+
<pre className={styles.systemContextPre}>
|
|
50
|
+
{displayContent}
|
|
51
|
+
{!expanded && hasMore && (
|
|
52
|
+
<span className={styles.systemContextEllipsis}>{"\u2026"}</span>
|
|
53
|
+
)}
|
|
54
|
+
</pre>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Renders a system-level event with timestamp. */
|
|
60
|
+
function SystemEvent({ time, content }: { time: string; content: string }): JSX.Element {
|
|
61
|
+
return (
|
|
62
|
+
<div className={styles.systemEvent}>
|
|
63
|
+
<span className={styles.systemTimestamp}>[{time}]</span> {content}
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Recursively extracts plain text from React children (for code block copy). */
|
|
69
|
+
export function extractText(node: ReactNode): string {
|
|
70
|
+
if (typeof node === "string") {
|
|
71
|
+
return node;
|
|
72
|
+
}
|
|
73
|
+
if (typeof node === "number") {
|
|
74
|
+
return String(node);
|
|
75
|
+
}
|
|
76
|
+
if (Array.isArray(node)) {
|
|
77
|
+
return node.map(extractText).join("");
|
|
78
|
+
}
|
|
79
|
+
if (node !== null && node !== undefined && typeof node === "object" && "props" in node) {
|
|
80
|
+
return extractText((node as { props: { children?: ReactNode } }).props.children);
|
|
81
|
+
}
|
|
82
|
+
return "";
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Props passed by react-markdown to component overrides. */
|
|
86
|
+
interface PreProps extends React.HTMLAttributes<HTMLPreElement> {
|
|
87
|
+
children?: ReactNode;
|
|
88
|
+
/** AST node injected by react-markdown — must not be spread onto the DOM element. */
|
|
89
|
+
node?: unknown;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Wraps markdown `<pre>` blocks with a CopyButton for code-only copy. */
|
|
93
|
+
function CodeBlockWrapper({ children, node, ...preProps }: PreProps): JSX.Element {
|
|
94
|
+
// node is destructured solely to exclude it from the DOM spread
|
|
95
|
+
if (node === undefined) { /* intentionally unused */ }
|
|
96
|
+
const rawText = extractText(children);
|
|
97
|
+
return (
|
|
98
|
+
<div className={styles.codeBlockWrapper}>
|
|
99
|
+
<pre {...preProps}>{children}</pre>
|
|
100
|
+
<CopyButton text={rawText} data-testid="copy-code-block" className={styles.codeBlockCopyButton} />
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Markdown component overrides for adding copy buttons to code blocks. */
|
|
106
|
+
const markdownComponents: Record<string, typeof CodeBlockWrapper> = {
|
|
107
|
+
pre: CodeBlockWrapper,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** Renders an assistant text output event with markdown formatting. */
|
|
111
|
+
function TextEvent({ content }: { content: string }): JSX.Element {
|
|
112
|
+
return (
|
|
113
|
+
<div className={styles.textEvent}>
|
|
114
|
+
<Markdown remarkPlugins={[remarkGfm]} rehypePlugins={[rehypePrismPlus]} components={markdownComponents}>
|
|
115
|
+
{content}
|
|
116
|
+
</Markdown>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ToolUseEvent and ToolResultEvent have been replaced by the ToolCard component
|
|
122
|
+
// in packages/web/src/components/tools/. See ToolCard.tsx for the router and
|
|
123
|
+
// individual card components (FileReadCard, FileEditCard, ShellCard, etc.).
|
|
124
|
+
|
|
125
|
+
/** Renders an error event with red styling. */
|
|
126
|
+
function ErrorEvent({ content }: { content: string }): JSX.Element {
|
|
127
|
+
return (
|
|
128
|
+
<div className={styles.errorEvent}>
|
|
129
|
+
Error: {content}
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** Renders a status change event with separator lines. */
|
|
135
|
+
function StatusEvent({ content }: { content: string }): JSX.Element {
|
|
136
|
+
return (
|
|
137
|
+
<div className={styles.statusEvent}>
|
|
138
|
+
--- {content} ---
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Renders a user input event, right-aligned to distinguish it from agent output. */
|
|
144
|
+
function UserInputEvent({ content }: { content: string }): JSX.Element {
|
|
145
|
+
return (
|
|
146
|
+
<div className={styles.userInputEvent}>
|
|
147
|
+
<span className={styles.userInputContent}>{content}</span>
|
|
148
|
+
</div>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/** Renders a signal event (e.g. SIGCHLD) as a left-bordered banner. */
|
|
153
|
+
function SignalEvent({ content }: { content: string }): JSX.Element {
|
|
154
|
+
return (
|
|
155
|
+
<div className={styles.signalEvent} data-testid="signal-event">
|
|
156
|
+
<span className={styles.signalBadge}>SIGNAL</span>
|
|
157
|
+
<span className={styles.signalContent}>{content}</span>
|
|
158
|
+
</div>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/** Renders a usage event as a compact cost badge. */
|
|
163
|
+
function UsageEvent({ content }: { content: string }): JSX.Element {
|
|
164
|
+
let label = content;
|
|
165
|
+
try {
|
|
166
|
+
const data = JSON.parse(content) as Record<string, unknown>;
|
|
167
|
+
const inTok = Number(data.input_tokens) || 0;
|
|
168
|
+
const outTok = Number(data.output_tokens) || 0;
|
|
169
|
+
const tokens = formatTokens(inTok + outTok);
|
|
170
|
+
const cost = formatCost(Number(data.cost_millicents) || 0);
|
|
171
|
+
label = `${tokens} tokens \u00b7 ${cost}`;
|
|
172
|
+
} catch { /* show raw content if JSON fails */ }
|
|
173
|
+
return (
|
|
174
|
+
<div className={styles.usageEvent} data-testid="usage-event">
|
|
175
|
+
<span className={styles.usageBadge}>{label}</span>
|
|
176
|
+
</div>
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Renders an unrecognized event type. */
|
|
181
|
+
function DefaultEvent({ content }: { content: string }): JSX.Element {
|
|
182
|
+
return (
|
|
183
|
+
<div className={styles.defaultEvent}>{content}</div>
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// --- Main component ---
|
|
188
|
+
|
|
189
|
+
/** Renders a single session event, dispatching to the appropriate type-specific renderer. */
|
|
190
|
+
export function EventRenderer({ event, toolUseCtx, settled }: Props): JSX.Element {
|
|
191
|
+
const time = new Date(event.timestamp).toLocaleTimeString();
|
|
192
|
+
|
|
193
|
+
switch (event.eventType) {
|
|
194
|
+
case "system": {
|
|
195
|
+
// Detect system context events via the raw metadata marker
|
|
196
|
+
if (event.raw) {
|
|
197
|
+
try {
|
|
198
|
+
const rawData = JSON.parse(event.raw) as Record<string, unknown>;
|
|
199
|
+
if (rawData.systemContext === true) {
|
|
200
|
+
return <SystemContextEvent content={event.content} />;
|
|
201
|
+
}
|
|
202
|
+
} catch { /* not JSON, render as normal system event */ }
|
|
203
|
+
}
|
|
204
|
+
return <SystemEvent time={time} content={event.content} />;
|
|
205
|
+
}
|
|
206
|
+
case "text":
|
|
207
|
+
case "output":
|
|
208
|
+
return <TextEvent content={event.content} />;
|
|
209
|
+
case "tool_use": {
|
|
210
|
+
let tool = "";
|
|
211
|
+
let args: unknown = {};
|
|
212
|
+
try {
|
|
213
|
+
const parsed = JSON.parse(event.content) as { tool?: string; args?: unknown };
|
|
214
|
+
tool = parsed.tool || "";
|
|
215
|
+
args = parsed.args;
|
|
216
|
+
} catch { /* fallback to empty */ }
|
|
217
|
+
// When settled, pass empty result so the card shows as completed (no spinner)
|
|
218
|
+
// rather than in-progress. This handles Claude Code which emits results as text.
|
|
219
|
+
return <ToolCard tool={tool} args={args} result={settled ? "" : undefined} />;
|
|
220
|
+
}
|
|
221
|
+
case "tool_result": {
|
|
222
|
+
// When paired, toolUseCtx provides the tool name, args, and optional detailedResult.
|
|
223
|
+
// When unpaired, fall back to a generic display.
|
|
224
|
+
let isError = false;
|
|
225
|
+
if (event.raw) {
|
|
226
|
+
try {
|
|
227
|
+
const rawData = JSON.parse(event.raw) as Record<string, unknown>;
|
|
228
|
+
isError = rawData.is_error === true;
|
|
229
|
+
} catch { /* ignore */ }
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Try to extract displayable content from JSON-wrapped results.
|
|
233
|
+
// Guard with startsWith check to avoid throwing on plain text content.
|
|
234
|
+
let resultContent = event.content;
|
|
235
|
+
if (event.content.trimStart().startsWith("{")) {
|
|
236
|
+
try {
|
|
237
|
+
const parsed = JSON.parse(event.content) as Record<string, unknown>;
|
|
238
|
+
if (typeof parsed.content === "string") {
|
|
239
|
+
resultContent = parsed.content;
|
|
240
|
+
}
|
|
241
|
+
} catch { /* content looks like JSON but isn't — use as-is */ }
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (toolUseCtx) {
|
|
245
|
+
return (
|
|
246
|
+
<ToolCard
|
|
247
|
+
tool={toolUseCtx.tool}
|
|
248
|
+
args={toolUseCtx.args}
|
|
249
|
+
result={resultContent}
|
|
250
|
+
isError={isError}
|
|
251
|
+
detailedResult={toolUseCtx.detailedResult}
|
|
252
|
+
/>
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
// Unpaired tool_result — use generic card with fallback label
|
|
256
|
+
return <ToolCard tool="Tool output" args={undefined} result={resultContent} isError={isError} />;
|
|
257
|
+
}
|
|
258
|
+
case "error":
|
|
259
|
+
return <ErrorEvent content={event.content} />;
|
|
260
|
+
case "status":
|
|
261
|
+
return <StatusEvent content={event.content} />;
|
|
262
|
+
case "user_input":
|
|
263
|
+
return <UserInputEvent content={event.content} />;
|
|
264
|
+
case "signal":
|
|
265
|
+
return <SignalEvent content={event.content} />;
|
|
266
|
+
case "usage":
|
|
267
|
+
return <UsageEvent content={event.content} />;
|
|
268
|
+
default:
|
|
269
|
+
return <DefaultEvent content={event.content} />;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
@use '../../styles/mixins' as *;
|
|
2
|
+
|
|
3
|
+
// =============================================================================
|
|
4
|
+
// EventStream — scrollable event list with smart scroll + direction toggle
|
|
5
|
+
// =============================================================================
|
|
6
|
+
|
|
7
|
+
.wrapper {
|
|
8
|
+
position: relative;
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
flex: 1;
|
|
12
|
+
overflow: hidden;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.scrollContainer {
|
|
16
|
+
flex: 1;
|
|
17
|
+
overflow: auto;
|
|
18
|
+
padding: var(--space-md);
|
|
19
|
+
// Extra top padding so the first event's hover action bar (positioned
|
|
20
|
+
// above via top: -28px) is not clipped by the scroll container.
|
|
21
|
+
padding-top: calc(var(--space-md) + 28px);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Extra bottom padding when floating action bar is visible, so the last event
|
|
25
|
+
// is not occluded by the bar.
|
|
26
|
+
.selectingPadding {
|
|
27
|
+
padding-bottom: 64px;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
.toolbar {
|
|
31
|
+
position: absolute;
|
|
32
|
+
top: var(--space-sm);
|
|
33
|
+
right: var(--space-md);
|
|
34
|
+
z-index: 10;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.directionToggle {
|
|
38
|
+
@include btn-ghost(var(--text-secondary));
|
|
39
|
+
@include surface-inset;
|
|
40
|
+
width: 28px;
|
|
41
|
+
height: 28px;
|
|
42
|
+
display: flex;
|
|
43
|
+
align-items: center;
|
|
44
|
+
justify-content: center;
|
|
45
|
+
font-size: var(--font-size-sm);
|
|
46
|
+
padding: 0;
|
|
47
|
+
border-radius: var(--radius-sm);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
.scrollToAnchor {
|
|
51
|
+
position: absolute;
|
|
52
|
+
z-index: 10;
|
|
53
|
+
right: var(--space-md);
|
|
54
|
+
@include surface-card;
|
|
55
|
+
padding: var(--space-xs) var(--space-sm);
|
|
56
|
+
cursor: pointer;
|
|
57
|
+
display: flex;
|
|
58
|
+
align-items: center;
|
|
59
|
+
gap: var(--space-xs);
|
|
60
|
+
font-size: var(--font-size-sm);
|
|
61
|
+
color: var(--text-secondary);
|
|
62
|
+
transition: color var(--transition-fast);
|
|
63
|
+
|
|
64
|
+
&:hover {
|
|
65
|
+
color: var(--text-primary);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.scrollToAnchorBottom {
|
|
70
|
+
bottom: var(--space-md);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.scrollToAnchorTop {
|
|
74
|
+
top: calc(var(--space-sm) + 36px);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ---------------------------------------------------------------------------
|
|
78
|
+
// Event overflow warning (shown when events exceed the 5,000 in-memory cap)
|
|
79
|
+
// ---------------------------------------------------------------------------
|
|
80
|
+
|
|
81
|
+
.eventOverflowWarning {
|
|
82
|
+
display: flex;
|
|
83
|
+
align-items: center;
|
|
84
|
+
gap: var(--space-sm);
|
|
85
|
+
padding: var(--space-xs) var(--space-sm);
|
|
86
|
+
margin-bottom: var(--space-sm);
|
|
87
|
+
border-radius: 4px;
|
|
88
|
+
background: color-mix(in srgb, var(--accent-yellow) 12%, transparent);
|
|
89
|
+
border: 1px solid color-mix(in srgb, var(--accent-yellow) 35%, transparent);
|
|
90
|
+
color: var(--accent-yellow);
|
|
91
|
+
font-size: var(--font-size-sm);
|
|
92
|
+
font-family: var(--font-mono);
|
|
93
|
+
}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { expect, fn, userEvent } from "@storybook/test";
|
|
3
|
+
import { EventStream } from "./EventStream.js";
|
|
4
|
+
import type { DisplayEvent } from "../../utils/sessionEvents.js";
|
|
5
|
+
import { makeEvent, makeSession, makeEnvironment } from "../../test-utils/storybook-helpers.js";
|
|
6
|
+
|
|
7
|
+
const sampleEvents: DisplayEvent[] = [
|
|
8
|
+
makeEvent({ eventType: "text", content: "First message", timestamp: "2026-01-01T00:00:01Z" }),
|
|
9
|
+
makeEvent({ eventType: "text", content: "Second message", timestamp: "2026-01-01T00:00:02Z" }),
|
|
10
|
+
makeEvent({ eventType: "text", content: "Third message", timestamp: "2026-01-01T00:00:03Z" }),
|
|
11
|
+
];
|
|
12
|
+
|
|
13
|
+
/** A richer set of events including non-content types for selection mode tests. */
|
|
14
|
+
const mixedEvents: DisplayEvent[] = [
|
|
15
|
+
makeEvent({ eventType: "user_input", content: "Fix the bug", timestamp: "2026-01-01T00:00:01Z" }),
|
|
16
|
+
makeEvent({ eventType: "text", content: "Looking into it.", timestamp: "2026-01-01T00:00:02Z" }),
|
|
17
|
+
makeEvent({ eventType: "status", content: "running", timestamp: "2026-01-01T00:00:03Z" }),
|
|
18
|
+
makeEvent({ eventType: "text", content: "Found the issue in auth.ts", timestamp: "2026-01-01T00:00:04Z" }),
|
|
19
|
+
makeEvent({ eventType: "error", content: "Test failed", timestamp: "2026-01-01T00:00:05Z" }),
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const meta: Meta<typeof EventStream> = {
|
|
23
|
+
component: EventStream,
|
|
24
|
+
title: "Grackle/Display/EventStream",
|
|
25
|
+
tags: ["autodocs"],
|
|
26
|
+
args: {
|
|
27
|
+
events: sampleEvents,
|
|
28
|
+
eventsDropped: 0,
|
|
29
|
+
onShowToast: fn(),
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
export default meta;
|
|
33
|
+
type Story = StoryObj<typeof meta>;
|
|
34
|
+
|
|
35
|
+
/** Default stream with events in chronological order. */
|
|
36
|
+
export const Default: Story = {};
|
|
37
|
+
|
|
38
|
+
/** Direction toggle button is present. */
|
|
39
|
+
export const DirectionToggle: Story = {
|
|
40
|
+
play: async ({ canvas }) => {
|
|
41
|
+
const toggle = canvas.getByTestId("direction-toggle");
|
|
42
|
+
await expect(toggle).toBeInTheDocument();
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/** Empty state renders when no events. */
|
|
47
|
+
export const EmptyState: Story = {
|
|
48
|
+
args: {
|
|
49
|
+
events: [],
|
|
50
|
+
emptyState: <div data-testid="custom-empty">No events yet</div>,
|
|
51
|
+
},
|
|
52
|
+
play: async ({ canvas }) => {
|
|
53
|
+
await expect(canvas.getByTestId("custom-empty")).toBeInTheDocument();
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/** Hover over a content event to see action buttons. */
|
|
58
|
+
export const HoverActions: Story = {
|
|
59
|
+
play: async ({ canvas }) => {
|
|
60
|
+
// Hover actions should exist in the DOM (opacity controlled by CSS)
|
|
61
|
+
const hoverRows = canvas.getAllByTestId("event-hover-row");
|
|
62
|
+
await expect(hoverRows.length).toBeGreaterThan(0);
|
|
63
|
+
|
|
64
|
+
// Each content event should have copy and select buttons
|
|
65
|
+
const copyButtons = canvas.getAllByTestId("event-hover-copy");
|
|
66
|
+
await expect(copyButtons.length).toBe(sampleEvents.length);
|
|
67
|
+
|
|
68
|
+
const selectButtons = canvas.getAllByTestId("event-hover-select");
|
|
69
|
+
await expect(selectButtons.length).toBe(sampleEvents.length);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/** Non-content events (status) do not get hover actions. */
|
|
74
|
+
export const NonContentNoHover: Story = {
|
|
75
|
+
args: {
|
|
76
|
+
events: mixedEvents,
|
|
77
|
+
},
|
|
78
|
+
play: async ({ canvas }) => {
|
|
79
|
+
// 4 content events out of 5 total (status is not content-bearing)
|
|
80
|
+
const hoverRows = canvas.getAllByTestId("event-hover-row");
|
|
81
|
+
await expect(hoverRows.length).toBe(4);
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/** Clicking Select enters selection mode with floating action bar. */
|
|
86
|
+
export const SelectionMode: Story = {
|
|
87
|
+
args: {
|
|
88
|
+
events: mixedEvents,
|
|
89
|
+
},
|
|
90
|
+
play: async ({ canvas }) => {
|
|
91
|
+
// Click the Select button on the first event
|
|
92
|
+
const selectButtons = canvas.getAllByTestId("event-hover-select");
|
|
93
|
+
await userEvent.click(selectButtons[0]);
|
|
94
|
+
|
|
95
|
+
// Floating action bar should appear
|
|
96
|
+
const bar = canvas.getByTestId("floating-action-bar");
|
|
97
|
+
await expect(bar).toBeInTheDocument();
|
|
98
|
+
|
|
99
|
+
// Count should show 1 selected
|
|
100
|
+
const count = canvas.getByTestId("floating-bar-count");
|
|
101
|
+
await expect(count).toHaveTextContent("1 selected");
|
|
102
|
+
|
|
103
|
+
// Checkboxes should be visible
|
|
104
|
+
const checkboxes = canvas.getAllByTestId("event-select-checkbox");
|
|
105
|
+
await expect(checkboxes.length).toBe(4); // 4 content-bearing events
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
/** Select multiple events and verify count updates. */
|
|
110
|
+
export const MultiSelect: Story = {
|
|
111
|
+
args: {
|
|
112
|
+
events: mixedEvents,
|
|
113
|
+
},
|
|
114
|
+
play: async ({ canvas }) => {
|
|
115
|
+
// Enter selection mode via first event
|
|
116
|
+
const selectButtons = canvas.getAllByTestId("event-hover-select");
|
|
117
|
+
await userEvent.click(selectButtons[0]);
|
|
118
|
+
|
|
119
|
+
// Click second checkbox to select it too
|
|
120
|
+
const checkboxes = canvas.getAllByTestId("event-select-checkbox");
|
|
121
|
+
await userEvent.click(checkboxes[1]);
|
|
122
|
+
|
|
123
|
+
// Count should show 2
|
|
124
|
+
const count = canvas.getByTestId("floating-bar-count");
|
|
125
|
+
await expect(count).toHaveTextContent("2 selected");
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
/** Select All selects all content-bearing events. */
|
|
130
|
+
export const SelectAll: Story = {
|
|
131
|
+
args: {
|
|
132
|
+
events: mixedEvents,
|
|
133
|
+
},
|
|
134
|
+
play: async ({ canvas }) => {
|
|
135
|
+
// Enter selection mode
|
|
136
|
+
const selectButtons = canvas.getAllByTestId("event-hover-select");
|
|
137
|
+
await userEvent.click(selectButtons[0]);
|
|
138
|
+
|
|
139
|
+
// Click "Select all"
|
|
140
|
+
const selectAllBtn = canvas.getByTestId("floating-bar-select-all");
|
|
141
|
+
await expect(selectAllBtn).toHaveTextContent("Select all");
|
|
142
|
+
await userEvent.click(selectAllBtn);
|
|
143
|
+
|
|
144
|
+
// Count should show all content-bearing events (4 of 5)
|
|
145
|
+
const count = canvas.getByTestId("floating-bar-count");
|
|
146
|
+
await expect(count).toHaveTextContent("4 selected");
|
|
147
|
+
|
|
148
|
+
// Button should now say "Deselect all"
|
|
149
|
+
await expect(selectAllBtn).toHaveTextContent("Deselect all");
|
|
150
|
+
},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/** Forward button appears in floating bar when sessions + onForward are provided. */
|
|
154
|
+
export const ForwardButtonVisible: Story = {
|
|
155
|
+
args: {
|
|
156
|
+
events: mixedEvents,
|
|
157
|
+
sessions: [
|
|
158
|
+
makeSession({ id: "sess-target", environmentId: "env-1", status: "running", prompt: "Another task" }),
|
|
159
|
+
],
|
|
160
|
+
environments: [makeEnvironment({ id: "env-1", displayName: "Production" })],
|
|
161
|
+
currentSessionId: "sess-current",
|
|
162
|
+
onForward: fn(),
|
|
163
|
+
},
|
|
164
|
+
play: async ({ canvas }) => {
|
|
165
|
+
// Enter selection mode
|
|
166
|
+
const selectButtons = canvas.getAllByTestId("event-hover-select");
|
|
167
|
+
await userEvent.click(selectButtons[0]);
|
|
168
|
+
|
|
169
|
+
// Forward button should be present and enabled (there is an active target session)
|
|
170
|
+
const forwardBtn = canvas.getByTestId("floating-bar-forward");
|
|
171
|
+
await expect(forwardBtn).toBeInTheDocument();
|
|
172
|
+
await expect(forwardBtn).toBeEnabled();
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/** Forward button is disabled when no other active sessions exist. */
|
|
177
|
+
export const ForwardButtonDisabled: Story = {
|
|
178
|
+
args: {
|
|
179
|
+
events: mixedEvents,
|
|
180
|
+
sessions: [],
|
|
181
|
+
environments: [],
|
|
182
|
+
currentSessionId: "sess-current",
|
|
183
|
+
onForward: fn(),
|
|
184
|
+
},
|
|
185
|
+
play: async ({ canvas }) => {
|
|
186
|
+
const selectButtons = canvas.getAllByTestId("event-hover-select");
|
|
187
|
+
await userEvent.click(selectButtons[0]);
|
|
188
|
+
|
|
189
|
+
const forwardBtn = canvas.getByTestId("floating-bar-forward");
|
|
190
|
+
await expect(forwardBtn).toHaveAttribute("aria-disabled", "true");
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/** Clicking Forward opens the session picker dialog. */
|
|
195
|
+
export const ForwardOpensSessionPicker: Story = {
|
|
196
|
+
args: {
|
|
197
|
+
events: mixedEvents,
|
|
198
|
+
sessions: [
|
|
199
|
+
makeSession({ id: "sess-target", environmentId: "env-1", status: "idle", prompt: "Other task" }),
|
|
200
|
+
],
|
|
201
|
+
environments: [makeEnvironment({ id: "env-1", displayName: "Dev" })],
|
|
202
|
+
currentSessionId: "sess-current",
|
|
203
|
+
onForward: fn(),
|
|
204
|
+
},
|
|
205
|
+
play: async ({ canvas }) => {
|
|
206
|
+
// Enter selection mode
|
|
207
|
+
const selectButtons = canvas.getAllByTestId("event-hover-select");
|
|
208
|
+
await userEvent.click(selectButtons[0]);
|
|
209
|
+
|
|
210
|
+
// Click Forward
|
|
211
|
+
const forwardBtn = canvas.getByTestId("floating-bar-forward");
|
|
212
|
+
await userEvent.click(forwardBtn);
|
|
213
|
+
|
|
214
|
+
// Session picker dialog should appear
|
|
215
|
+
const dialog = canvas.getByTestId("session-picker-dialog");
|
|
216
|
+
await expect(dialog).toBeInTheDocument();
|
|
217
|
+
|
|
218
|
+
// The target session should be listed
|
|
219
|
+
await expect(canvas.getByTestId("session-picker-item-sess-target")).toBeInTheDocument();
|
|
220
|
+
},
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
/** Cancel exits selection mode -- checkboxes disappear. */
|
|
224
|
+
export const CancelSelection: Story = {
|
|
225
|
+
args: {
|
|
226
|
+
events: mixedEvents,
|
|
227
|
+
},
|
|
228
|
+
play: async ({ canvas, canvasElement }) => {
|
|
229
|
+
// Enter selection mode
|
|
230
|
+
const selectButtons = canvas.getAllByTestId("event-hover-select");
|
|
231
|
+
await userEvent.click(selectButtons[0]);
|
|
232
|
+
|
|
233
|
+
// Verify checkboxes are present (selection mode active)
|
|
234
|
+
const checkboxes = canvas.getAllByTestId("event-select-checkbox");
|
|
235
|
+
await expect(checkboxes.length).toBe(4);
|
|
236
|
+
|
|
237
|
+
// Click cancel
|
|
238
|
+
const cancelBtn = canvas.getByTestId("floating-bar-cancel");
|
|
239
|
+
await userEvent.click(cancelBtn);
|
|
240
|
+
|
|
241
|
+
// Selection mode exited: checkboxes should be gone, hover rows should be back
|
|
242
|
+
const checkboxesAfter = canvasElement.querySelectorAll("[data-testid='event-select-checkbox']");
|
|
243
|
+
await expect(checkboxesAfter.length).toBe(0);
|
|
244
|
+
|
|
245
|
+
// Hover rows should be back (content events re-render in normal mode)
|
|
246
|
+
const hoverRows = canvas.getAllByTestId("event-hover-row");
|
|
247
|
+
await expect(hoverRows.length).toBe(4);
|
|
248
|
+
},
|
|
249
|
+
};
|