@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,165 @@
|
|
|
1
|
+
import { useState, type JSX } from "react";
|
|
2
|
+
import type { ToolCardProps } from "./ToolCardProps.js";
|
|
3
|
+
import { extractBareName } from "./classifyTool.js";
|
|
4
|
+
import styles from "./toolCards.module.scss";
|
|
5
|
+
|
|
6
|
+
/** Shape of a knowledge search result node. */
|
|
7
|
+
interface KnowledgeResult {
|
|
8
|
+
score?: number;
|
|
9
|
+
node?: KnowledgeNode;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Shape of a knowledge graph node. */
|
|
13
|
+
interface KnowledgeNode {
|
|
14
|
+
id?: string;
|
|
15
|
+
kind?: string;
|
|
16
|
+
label?: string;
|
|
17
|
+
title?: string;
|
|
18
|
+
category?: string;
|
|
19
|
+
content?: string;
|
|
20
|
+
tags?: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Extracts knowledge-relevant fields from tool args. */
|
|
24
|
+
function getArgs(args: unknown): { query?: string; id?: string } {
|
|
25
|
+
if (args === null || args === undefined || typeof args !== "object") {
|
|
26
|
+
return {};
|
|
27
|
+
}
|
|
28
|
+
const a = args as Record<string, unknown>;
|
|
29
|
+
return {
|
|
30
|
+
query: typeof a.query === "string" ? a.query : undefined,
|
|
31
|
+
id: typeof a.id === "string" ? a.id : undefined,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Parses knowledge result. Could be search results or a single node. */
|
|
36
|
+
function parseResult(result: string | undefined): { results?: KnowledgeResult[]; node?: KnowledgeNode; edgeCount?: number } {
|
|
37
|
+
if (!result) {
|
|
38
|
+
return {};
|
|
39
|
+
}
|
|
40
|
+
try {
|
|
41
|
+
const parsed: unknown = JSON.parse(result);
|
|
42
|
+
if (typeof parsed !== "object" || parsed === null) {
|
|
43
|
+
return {};
|
|
44
|
+
}
|
|
45
|
+
const obj = parsed as Record<string, unknown>;
|
|
46
|
+
// knowledge_search returns { results: [...], neighbors, neighborEdges }
|
|
47
|
+
if (Array.isArray(obj.results)) {
|
|
48
|
+
return { results: (obj.results as unknown[]).filter((v): v is KnowledgeResult => v !== null && typeof v === "object") };
|
|
49
|
+
}
|
|
50
|
+
// knowledge_get_node returns { node, edges, neighbors }
|
|
51
|
+
if (typeof obj.node === "object" && obj.node !== null) {
|
|
52
|
+
const edges = Array.isArray(obj.edges) ? obj.edges.length : 0;
|
|
53
|
+
return { node: obj.node as KnowledgeNode, edgeCount: edges };
|
|
54
|
+
}
|
|
55
|
+
// knowledge_create_node returns { id, title, category }
|
|
56
|
+
if (typeof obj.id === "string") {
|
|
57
|
+
return { node: obj as KnowledgeNode };
|
|
58
|
+
}
|
|
59
|
+
} catch { /* fall through */ }
|
|
60
|
+
return {};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Number of results shown when collapsed. */
|
|
64
|
+
const PREVIEW_COUNT: number = 5;
|
|
65
|
+
|
|
66
|
+
/** Renders a knowledge tool call (knowledge_search, knowledge_get_node) with structured display. */
|
|
67
|
+
export function KnowledgeCard({ tool, args, result, isError }: ToolCardProps): JSX.Element {
|
|
68
|
+
const [expanded, setExpanded] = useState(false);
|
|
69
|
+
const bareName = extractBareName(tool);
|
|
70
|
+
const argData = getArgs(args);
|
|
71
|
+
const inProgress = result === undefined;
|
|
72
|
+
const resultData = parseResult(result);
|
|
73
|
+
|
|
74
|
+
return (
|
|
75
|
+
<div
|
|
76
|
+
className={`${styles.card} ${isError ? styles.cardRed : styles.cardPurple} ${inProgress ? styles.inProgress : ""}`}
|
|
77
|
+
data-testid="tool-card-knowledge"
|
|
78
|
+
>
|
|
79
|
+
<div className={styles.header}>
|
|
80
|
+
<span className={styles.icon} aria-hidden="true">🧠</span>
|
|
81
|
+
<span className={styles.toolName} style={{ color: "var(--accent-purple, #a78bfa)" }}>
|
|
82
|
+
{bareName}
|
|
83
|
+
</span>
|
|
84
|
+
{argData.query && (
|
|
85
|
+
<span className={styles.fileName} data-testid="tool-card-knowledge-query">
|
|
86
|
+
"{argData.query}"
|
|
87
|
+
</span>
|
|
88
|
+
)}
|
|
89
|
+
{argData.id && !argData.query && (
|
|
90
|
+
<span className={styles.fileName} data-testid="tool-card-knowledge-id">
|
|
91
|
+
{argData.id}
|
|
92
|
+
</span>
|
|
93
|
+
)}
|
|
94
|
+
{resultData.results && (
|
|
95
|
+
<>
|
|
96
|
+
<span className={styles.spacer} />
|
|
97
|
+
<span className={styles.badge} data-testid="tool-card-knowledge-count">
|
|
98
|
+
{resultData.results.length} {resultData.results.length === 1 ? "result" : "results"}
|
|
99
|
+
</span>
|
|
100
|
+
</>
|
|
101
|
+
)}
|
|
102
|
+
{resultData.node && resultData.edgeCount !== undefined && (
|
|
103
|
+
<>
|
|
104
|
+
<span className={styles.spacer} />
|
|
105
|
+
<span className={styles.badge} data-testid="tool-card-knowledge-edges">
|
|
106
|
+
{resultData.edgeCount} {resultData.edgeCount === 1 ? "edge" : "edges"}
|
|
107
|
+
</span>
|
|
108
|
+
</>
|
|
109
|
+
)}
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
{/* In-progress: show args */}
|
|
113
|
+
{inProgress && args !== null && args !== undefined && !argData.query && !argData.id && (
|
|
114
|
+
<pre className={styles.pre} data-testid="tool-card-args">
|
|
115
|
+
{JSON.stringify(args, null, 2)}
|
|
116
|
+
</pre>
|
|
117
|
+
)}
|
|
118
|
+
|
|
119
|
+
{/* Error */}
|
|
120
|
+
{isError && result && (
|
|
121
|
+
<pre className={styles.pre} data-testid="tool-card-error">
|
|
122
|
+
{result}
|
|
123
|
+
</pre>
|
|
124
|
+
)}
|
|
125
|
+
|
|
126
|
+
{/* Search results */}
|
|
127
|
+
{!isError && resultData.results && resultData.results.length > 0 && (
|
|
128
|
+
<>
|
|
129
|
+
<pre className={styles.pre} data-testid="tool-card-knowledge-results">
|
|
130
|
+
{(expanded ? resultData.results : resultData.results.slice(0, PREVIEW_COUNT)).map((r) => {
|
|
131
|
+
const label = r.node?.title ?? r.node?.label ?? r.node?.id ?? "node";
|
|
132
|
+
const score = r.score !== undefined ? ` (${(r.score * 100).toFixed(0)}%)` : "";
|
|
133
|
+
return `${label}${score}`;
|
|
134
|
+
}).join("\n")}
|
|
135
|
+
</pre>
|
|
136
|
+
{resultData.results.length > PREVIEW_COUNT && (
|
|
137
|
+
<button
|
|
138
|
+
type="button"
|
|
139
|
+
className={styles.bodyToggle}
|
|
140
|
+
onClick={() => { setExpanded((v) => !v); }}
|
|
141
|
+
aria-expanded={expanded}
|
|
142
|
+
data-testid="tool-card-toggle"
|
|
143
|
+
>
|
|
144
|
+
<span className={`${styles.chevron} ${expanded ? styles.chevronExpanded : ""}`}>▸</span>
|
|
145
|
+
{expanded ? "collapse" : `${resultData.results.length - PREVIEW_COUNT} more results`}
|
|
146
|
+
</button>
|
|
147
|
+
)}
|
|
148
|
+
</>
|
|
149
|
+
)}
|
|
150
|
+
|
|
151
|
+
{/* Single node */}
|
|
152
|
+
{!isError && resultData.node && !resultData.results && (
|
|
153
|
+
<pre className={styles.pre} data-testid="tool-card-knowledge-node">
|
|
154
|
+
{[
|
|
155
|
+
resultData.node.id ? `id: ${resultData.node.id}` : null,
|
|
156
|
+
resultData.node.title ? `title: ${resultData.node.title}` : null,
|
|
157
|
+
resultData.node.category ? `category: ${resultData.node.category}` : null,
|
|
158
|
+
resultData.node.kind ? `kind: ${resultData.node.kind}` : null,
|
|
159
|
+
resultData.node.content ? `content: ${resultData.node.content.length > 100 ? resultData.node.content.slice(0, 100) + "..." : resultData.node.content}` : null,
|
|
160
|
+
].filter(Boolean).join("\n")}
|
|
161
|
+
</pre>
|
|
162
|
+
)}
|
|
163
|
+
</div>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { expect } from "@storybook/test";
|
|
3
|
+
import { MetadataCard } from "./MetadataCard.js";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof MetadataCard> = {
|
|
6
|
+
component: MetadataCard,
|
|
7
|
+
title: "Grackle/Tools/MetadataCard",
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
};
|
|
10
|
+
export default meta;
|
|
11
|
+
type Story = StoryObj<typeof meta>;
|
|
12
|
+
|
|
13
|
+
export const ReportIntent: Story = {
|
|
14
|
+
args: {
|
|
15
|
+
tool: "report_intent",
|
|
16
|
+
args: { intent: "Updating README getting started" },
|
|
17
|
+
},
|
|
18
|
+
play: async ({ canvas }) => {
|
|
19
|
+
await expect(canvas.getByTestId("tool-card-metadata")).toBeInTheDocument();
|
|
20
|
+
await expect(canvas.getByTestId("tool-card-metadata")).toHaveTextContent("Updating README getting started");
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export const UnknownMetadata: Story = {
|
|
25
|
+
args: {
|
|
26
|
+
tool: "report_intent",
|
|
27
|
+
args: { description: "Planning implementation" },
|
|
28
|
+
},
|
|
29
|
+
play: async ({ canvas }) => {
|
|
30
|
+
await expect(canvas.getByTestId("tool-card-metadata")).toHaveTextContent("Planning implementation");
|
|
31
|
+
},
|
|
32
|
+
};
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { JSX } from "react";
|
|
2
|
+
import { CornerDownRight } from "lucide-react";
|
|
3
|
+
import type { ToolCardProps } from "./ToolCardProps.js";
|
|
4
|
+
import { ICON_SM } from "../../utils/iconSize.js";
|
|
5
|
+
import styles from "./toolCards.module.scss";
|
|
6
|
+
|
|
7
|
+
/** Extracts a human-readable summary from metadata tool args. */
|
|
8
|
+
function getSummary(tool: string, args: unknown): string {
|
|
9
|
+
if (args === null || args === undefined || typeof args !== "object") {
|
|
10
|
+
return "";
|
|
11
|
+
}
|
|
12
|
+
const a = args as Record<string, unknown>;
|
|
13
|
+
|
|
14
|
+
// report_intent: show the intent value
|
|
15
|
+
if (typeof a.intent === "string") {
|
|
16
|
+
return a.intent;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// Generic: show first string value
|
|
20
|
+
for (const value of Object.values(a)) {
|
|
21
|
+
if (typeof value === "string") {
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return tool;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Renders a metadata/intent tool call as a minimal inline annotation. */
|
|
30
|
+
export function MetadataCard({ tool, args }: ToolCardProps): JSX.Element {
|
|
31
|
+
const summary = getSummary(tool, args);
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
<div className={styles.metadata} data-testid="tool-card-metadata">
|
|
35
|
+
<span className={styles.metadataPrefix}><CornerDownRight size={ICON_SM} /></span>
|
|
36
|
+
{summary}
|
|
37
|
+
</div>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { expect } from "@storybook/test";
|
|
3
|
+
import { SearchCard } from "./SearchCard.js";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof SearchCard> = {
|
|
6
|
+
component: SearchCard,
|
|
7
|
+
title: "Grackle/Tools/SearchCard",
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
};
|
|
10
|
+
export default meta;
|
|
11
|
+
type Story = StoryObj<typeof meta>;
|
|
12
|
+
|
|
13
|
+
export const GrepWithMatches: Story = {
|
|
14
|
+
name: "Grep - multiple matches",
|
|
15
|
+
args: {
|
|
16
|
+
tool: "Grep",
|
|
17
|
+
args: { pattern: "verifyToken", path: "src/" },
|
|
18
|
+
result: "src/middleware/auth.ts:14: export function verifyToken(req: Request) {\nsrc/routes/protected.ts:8: const user = verifyToken(req);\nsrc/tests/auth.test.ts:22: describe('verifyToken', () => {",
|
|
19
|
+
},
|
|
20
|
+
play: async ({ canvas }) => {
|
|
21
|
+
await expect(canvas.getByTestId("tool-card-search")).toBeInTheDocument();
|
|
22
|
+
await expect(canvas.getByTestId("tool-card-pattern")).toHaveTextContent('"verifyToken"');
|
|
23
|
+
await expect(canvas.getByTestId("tool-card-search-path")).toHaveTextContent("in src/");
|
|
24
|
+
await expect(canvas.getByTestId("tool-card-match-count")).toHaveTextContent("3 matches");
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export const GlobFileList: Story = {
|
|
29
|
+
name: "Glob - file list",
|
|
30
|
+
args: {
|
|
31
|
+
tool: "Glob",
|
|
32
|
+
args: { pattern: "src/**/*.test.ts" },
|
|
33
|
+
result: "src/auth.test.ts\nsrc/routes.test.ts\nsrc/middleware.test.ts\nsrc/utils.test.ts",
|
|
34
|
+
},
|
|
35
|
+
play: async ({ canvas }) => {
|
|
36
|
+
await expect(canvas.getByTestId("tool-card-match-count")).toHaveTextContent("4 matches");
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const NoMatches: Story = {
|
|
41
|
+
name: "No matches",
|
|
42
|
+
args: {
|
|
43
|
+
tool: "Grep",
|
|
44
|
+
args: { pattern: "nonExistentSymbol", path: "src/" },
|
|
45
|
+
result: "",
|
|
46
|
+
},
|
|
47
|
+
play: async ({ canvas }) => {
|
|
48
|
+
await expect(canvas.queryByTestId("tool-card-results")).not.toBeInTheDocument();
|
|
49
|
+
await expect(canvas.queryByTestId("tool-card-match-count")).not.toBeInTheDocument();
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
export const InProgress: Story = {
|
|
54
|
+
args: {
|
|
55
|
+
tool: "Grep",
|
|
56
|
+
args: { pattern: "TODO", path: "." },
|
|
57
|
+
},
|
|
58
|
+
play: async ({ canvas }) => {
|
|
59
|
+
const card = canvas.getByTestId("tool-card-search");
|
|
60
|
+
await expect(card.className).toContain("inProgress");
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export const ErrorResult: Story = {
|
|
65
|
+
args: {
|
|
66
|
+
tool: "Grep",
|
|
67
|
+
args: { pattern: "[invalid regex", path: "src/" },
|
|
68
|
+
result: "Error: invalid regex pattern",
|
|
69
|
+
isError: true,
|
|
70
|
+
},
|
|
71
|
+
play: async ({ canvas }) => {
|
|
72
|
+
await expect(canvas.getByTestId("tool-card-error")).toBeInTheDocument();
|
|
73
|
+
},
|
|
74
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { useState, type JSX } from "react";
|
|
2
|
+
import { ChevronRight, Search } from "lucide-react";
|
|
3
|
+
import type { ToolCardProps } from "./ToolCardProps.js";
|
|
4
|
+
import { ICON_SM, ICON_MD } from "../../utils/iconSize.js";
|
|
5
|
+
import styles from "./toolCards.module.scss";
|
|
6
|
+
|
|
7
|
+
/** Extracts search-relevant fields from tool args. */
|
|
8
|
+
function getSearchInfo(args: unknown): { pattern: string; path: string } {
|
|
9
|
+
if (args === null || args === undefined || typeof args !== "object") {
|
|
10
|
+
return { pattern: "", path: "" };
|
|
11
|
+
}
|
|
12
|
+
const a = args as Record<string, unknown>;
|
|
13
|
+
const pattern = typeof a.pattern === "string" ? a.pattern : "";
|
|
14
|
+
const path = typeof a.path === "string" ? a.path : "";
|
|
15
|
+
return { pattern, path };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Number of result lines shown when collapsed. */
|
|
19
|
+
const PREVIEW_LINES: number = 5;
|
|
20
|
+
|
|
21
|
+
/** Renders a search tool call (Grep, Glob) with pattern and match results. */
|
|
22
|
+
export function SearchCard({ tool, args, result, isError }: ToolCardProps): JSX.Element {
|
|
23
|
+
const [expanded, setExpanded] = useState(false);
|
|
24
|
+
const { pattern, path } = getSearchInfo(args);
|
|
25
|
+
const inProgress = result === undefined;
|
|
26
|
+
|
|
27
|
+
const lines = result?.split("\n").filter((l) => l.length > 0) ?? [];
|
|
28
|
+
const hasMore = lines.length > PREVIEW_LINES;
|
|
29
|
+
const displayLines = expanded ? lines : lines.slice(0, PREVIEW_LINES);
|
|
30
|
+
|
|
31
|
+
return (
|
|
32
|
+
<div
|
|
33
|
+
className={`${styles.card} ${isError ? styles.cardRed : styles.cardPurple} ${inProgress ? styles.inProgress : ""}`}
|
|
34
|
+
data-testid="tool-card-search"
|
|
35
|
+
>
|
|
36
|
+
<div className={styles.header}>
|
|
37
|
+
<span className={styles.icon}><Search size={ICON_MD} /></span>
|
|
38
|
+
<span className={styles.toolName} style={{ color: "var(--accent-purple, #a78bfa)" }}>{tool}</span>
|
|
39
|
+
{pattern && (
|
|
40
|
+
<span className={styles.fileName} data-testid="tool-card-pattern">
|
|
41
|
+
"{pattern}"
|
|
42
|
+
</span>
|
|
43
|
+
)}
|
|
44
|
+
{path && (
|
|
45
|
+
<span className={styles.fileName} style={{ flexShrink: 1 }} data-testid="tool-card-search-path">
|
|
46
|
+
in {path}
|
|
47
|
+
</span>
|
|
48
|
+
)}
|
|
49
|
+
{!inProgress && !isError && lines.length > 0 && (
|
|
50
|
+
<>
|
|
51
|
+
<span className={styles.spacer} />
|
|
52
|
+
<span className={styles.badge} data-testid="tool-card-match-count">
|
|
53
|
+
{lines.length} {lines.length === 1 ? "match" : "matches"}
|
|
54
|
+
</span>
|
|
55
|
+
</>
|
|
56
|
+
)}
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
{isError && result && (
|
|
60
|
+
<pre className={styles.pre} data-testid="tool-card-error">
|
|
61
|
+
{result}
|
|
62
|
+
</pre>
|
|
63
|
+
)}
|
|
64
|
+
|
|
65
|
+
{!isError && !inProgress && displayLines.length > 0 && (
|
|
66
|
+
<>
|
|
67
|
+
<pre className={styles.pre} data-testid="tool-card-results">
|
|
68
|
+
{displayLines.join("\n")}
|
|
69
|
+
</pre>
|
|
70
|
+
{hasMore && (
|
|
71
|
+
<button
|
|
72
|
+
type="button"
|
|
73
|
+
className={styles.bodyToggle}
|
|
74
|
+
onClick={() => { setExpanded((v) => !v); }}
|
|
75
|
+
aria-expanded={expanded}
|
|
76
|
+
data-testid="tool-card-toggle"
|
|
77
|
+
>
|
|
78
|
+
<span className={`${styles.chevron} ${expanded ? styles.chevronExpanded : ""}`} aria-hidden="true"><ChevronRight size={ICON_SM} /></span>
|
|
79
|
+
{expanded ? "collapse" : `${lines.length - PREVIEW_LINES} more matches`}
|
|
80
|
+
</button>
|
|
81
|
+
)}
|
|
82
|
+
</>
|
|
83
|
+
)}
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
+
import { expect, userEvent } from "@storybook/test";
|
|
3
|
+
import { ShellCard } from "./ShellCard.js";
|
|
4
|
+
|
|
5
|
+
const meta: Meta<typeof ShellCard> = {
|
|
6
|
+
component: ShellCard,
|
|
7
|
+
title: "Grackle/Tools/ShellCard",
|
|
8
|
+
tags: ["autodocs"],
|
|
9
|
+
};
|
|
10
|
+
export default meta;
|
|
11
|
+
type Story = StoryObj<typeof meta>;
|
|
12
|
+
|
|
13
|
+
export const SuccessWithOutput: Story = {
|
|
14
|
+
name: "Success - exit 0",
|
|
15
|
+
args: {
|
|
16
|
+
tool: "Bash",
|
|
17
|
+
args: { command: "npm test -- --grep auth" },
|
|
18
|
+
result: "[exit 0] > jest --grep auth\n\n PASS src/auth.test.ts\n ✓ validates JWT token (12ms)\n ✓ rejects expired token (3ms)\n ✓ handles missing header (1ms)\n\nTests: 3 passed, 3 total\nTime: 1.234s",
|
|
19
|
+
},
|
|
20
|
+
play: async ({ canvas }) => {
|
|
21
|
+
await expect(canvas.getByTestId("tool-card-shell")).toBeInTheDocument();
|
|
22
|
+
await expect(canvas.getByTestId("tool-card-command")).toHaveTextContent("npm test -- --grep auth");
|
|
23
|
+
await expect(canvas.getByTestId("tool-card-exit-code")).toHaveTextContent("exit 0");
|
|
24
|
+
// Should have toggle (>3 lines)
|
|
25
|
+
await expect(canvas.getByTestId("tool-card-toggle")).toBeInTheDocument();
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export const ErrorExit: Story = {
|
|
30
|
+
name: "Failure - exit 1",
|
|
31
|
+
args: {
|
|
32
|
+
tool: "Bash",
|
|
33
|
+
args: { command: "npm run build" },
|
|
34
|
+
result: "[exit 1] src/index.ts(14,5): error TS2304: Cannot find name 'foo'.",
|
|
35
|
+
isError: true,
|
|
36
|
+
},
|
|
37
|
+
play: async ({ canvas }) => {
|
|
38
|
+
await expect(canvas.getByTestId("tool-card-exit-code")).toHaveTextContent("exit 1");
|
|
39
|
+
const card = canvas.getByTestId("tool-card-shell");
|
|
40
|
+
await expect(card.className).toContain("cardRed");
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export const PowerShellWrapper: Story = {
|
|
45
|
+
name: "Codex - PowerShell wrapper simplified",
|
|
46
|
+
args: {
|
|
47
|
+
tool: "command_execution",
|
|
48
|
+
args: { command: '"C:\\\\Program Files\\\\PowerShell\\\\7\\\\pwsh.exe" -Command \'Get-Content -Path README.md\'' },
|
|
49
|
+
result: "[exit 0] # Grackle\n\nRun any AI coding agent...",
|
|
50
|
+
},
|
|
51
|
+
play: async ({ canvas }) => {
|
|
52
|
+
// Should strip the PowerShell wrapper
|
|
53
|
+
await expect(canvas.getByTestId("tool-card-command")).toHaveTextContent("Get-Content -Path README.md");
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
export const LongOutput: Story = {
|
|
58
|
+
name: "Long output - expand/collapse",
|
|
59
|
+
args: {
|
|
60
|
+
tool: "Bash",
|
|
61
|
+
args: { command: "git log --oneline -10" },
|
|
62
|
+
result: "abc1234 feat: add auth\ndef5678 fix: typo\nghi9012 chore: deps\njkl3456 docs: readme\nmno7890 test: add coverage\npqr1234 refactor: cleanup",
|
|
63
|
+
},
|
|
64
|
+
play: async ({ canvas }) => {
|
|
65
|
+
const toggle = canvas.getByTestId("tool-card-toggle");
|
|
66
|
+
await expect(toggle).toBeInTheDocument();
|
|
67
|
+
await expect(toggle.textContent).toContain("3 more lines");
|
|
68
|
+
|
|
69
|
+
await userEvent.click(toggle);
|
|
70
|
+
await expect(toggle.textContent).toContain("collapse");
|
|
71
|
+
|
|
72
|
+
await userEvent.click(toggle);
|
|
73
|
+
await expect(toggle.textContent).toContain("more lines");
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const InProgress: Story = {
|
|
78
|
+
args: {
|
|
79
|
+
tool: "Bash",
|
|
80
|
+
args: { command: "npm install" },
|
|
81
|
+
// No result yet
|
|
82
|
+
},
|
|
83
|
+
play: async ({ canvas }) => {
|
|
84
|
+
await expect(canvas.getByTestId("tool-card-pending")).toBeInTheDocument();
|
|
85
|
+
await expect(canvas.queryByTestId("tool-card-output")).not.toBeInTheDocument();
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
export const EmptyOutput: Story = {
|
|
90
|
+
name: "Exit 0 - no output",
|
|
91
|
+
args: {
|
|
92
|
+
tool: "Bash",
|
|
93
|
+
args: { command: "mkdir -p dist" },
|
|
94
|
+
result: "[exit 0] ",
|
|
95
|
+
},
|
|
96
|
+
play: async ({ canvas }) => {
|
|
97
|
+
await expect(canvas.getByTestId("tool-card-exit-code")).toHaveTextContent("exit 0");
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
export const NoExitCode: Story = {
|
|
102
|
+
name: "Plain output (no exit code prefix)",
|
|
103
|
+
args: {
|
|
104
|
+
tool: "Bash",
|
|
105
|
+
args: { command: "echo hello" },
|
|
106
|
+
result: "hello",
|
|
107
|
+
},
|
|
108
|
+
play: async ({ canvas }) => {
|
|
109
|
+
await expect(canvas.queryByTestId("tool-card-exit-code")).not.toBeInTheDocument();
|
|
110
|
+
await expect(canvas.getByTestId("tool-card-output")).toHaveTextContent("hello");
|
|
111
|
+
},
|
|
112
|
+
};
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { useState, type JSX } from "react";
|
|
2
|
+
import { Check, ChevronRight, Loader, X } from "lucide-react";
|
|
3
|
+
import type { ToolCardProps } from "./ToolCardProps.js";
|
|
4
|
+
import { parseShellOutput } from "./parseShellOutput.js";
|
|
5
|
+
import { ICON_SM } from "../../utils/iconSize.js";
|
|
6
|
+
import styles from "./toolCards.module.scss";
|
|
7
|
+
|
|
8
|
+
/** Extracts the command string from shell tool args. */
|
|
9
|
+
function getCommand(args: unknown): string {
|
|
10
|
+
if (args === null || args === undefined || typeof args !== "object") {
|
|
11
|
+
return "";
|
|
12
|
+
}
|
|
13
|
+
const a = args as Record<string, unknown>;
|
|
14
|
+
if (typeof a.command === "string") {
|
|
15
|
+
return a.command;
|
|
16
|
+
}
|
|
17
|
+
return "";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Simplifies a shell command for display.
|
|
22
|
+
*
|
|
23
|
+
* Strips PowerShell wrappers like `"C:\\Program Files\\PowerShell\\7\\pwsh.exe" -Command 'inner'`
|
|
24
|
+
* down to just the inner command.
|
|
25
|
+
*/
|
|
26
|
+
export function simplifyCommand(cmd: string): string {
|
|
27
|
+
// Match: "...pwsh.exe" -Command 'inner command'
|
|
28
|
+
const pwshMatch = /pwsh(?:\.exe)?["']?\s+-Command\s+'([^']*)'\s*$/i.exec(cmd);
|
|
29
|
+
if (pwshMatch) {
|
|
30
|
+
return pwshMatch[1];
|
|
31
|
+
}
|
|
32
|
+
// Match: "...pwsh.exe" -Command "inner command"
|
|
33
|
+
const pwshMatch2 = /pwsh(?:\.exe)?["']?\s+-Command\s+"([^"]*)"\s*$/i.exec(cmd);
|
|
34
|
+
if (pwshMatch2) {
|
|
35
|
+
return pwshMatch2[1];
|
|
36
|
+
}
|
|
37
|
+
return cmd;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Number of output lines shown when collapsed. */
|
|
41
|
+
const PREVIEW_LINES: number = 3;
|
|
42
|
+
|
|
43
|
+
/** Renders a shell command tool call with terminal-style output. */
|
|
44
|
+
export function ShellCard({ tool, args, result, isError }: ToolCardProps): JSX.Element {
|
|
45
|
+
const [expanded, setExpanded] = useState(false);
|
|
46
|
+
const rawCommand = getCommand(args);
|
|
47
|
+
const command = simplifyCommand(rawCommand);
|
|
48
|
+
const inProgress = result === undefined;
|
|
49
|
+
|
|
50
|
+
const parsed = result ? parseShellOutput(result) : null;
|
|
51
|
+
const outputLines = parsed?.output.split("\n") ?? [];
|
|
52
|
+
const hasMore = outputLines.length > PREVIEW_LINES;
|
|
53
|
+
const displayLines = expanded ? outputLines : outputLines.slice(0, PREVIEW_LINES);
|
|
54
|
+
const exitCode: number | undefined = parsed?.exitCode;
|
|
55
|
+
const derivedIsError: boolean = isError || (exitCode !== undefined && exitCode !== 0);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div
|
|
59
|
+
className={`${styles.card} ${derivedIsError ? styles.cardRed : styles.cardNeutral}`}
|
|
60
|
+
data-testid="tool-card-shell"
|
|
61
|
+
>
|
|
62
|
+
<div className={styles.header}>
|
|
63
|
+
<span className={styles.icon} style={{ color: "var(--text-secondary)", fontFamily: "var(--font-mono)", fontWeight: "bold" }}>$</span>
|
|
64
|
+
<span
|
|
65
|
+
className={styles.fileName}
|
|
66
|
+
style={{ flex: 1, color: "var(--text-primary)" }}
|
|
67
|
+
title={rawCommand !== command ? rawCommand : undefined}
|
|
68
|
+
data-testid="tool-card-command"
|
|
69
|
+
>
|
|
70
|
+
{command}
|
|
71
|
+
</span>
|
|
72
|
+
{exitCode !== undefined && (
|
|
73
|
+
<span
|
|
74
|
+
className={exitCode === 0 ? styles.exitOk : styles.exitError}
|
|
75
|
+
data-testid="tool-card-exit-code"
|
|
76
|
+
>
|
|
77
|
+
{exitCode === 0 ? <Check size={ICON_SM} aria-hidden="true" /> : <X size={ICON_SM} aria-hidden="true" />}{" "}exit {exitCode}
|
|
78
|
+
</span>
|
|
79
|
+
)}
|
|
80
|
+
{inProgress && (
|
|
81
|
+
<span className={styles.exitPending} data-testid="tool-card-pending"><Loader size={ICON_SM} aria-hidden="true" /></span>
|
|
82
|
+
)}
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
{!inProgress && parsed && parsed.output.length > 0 && (
|
|
86
|
+
<>
|
|
87
|
+
<pre className={styles.pre} data-testid="tool-card-output">
|
|
88
|
+
{displayLines.join("\n")}
|
|
89
|
+
</pre>
|
|
90
|
+
{hasMore && (
|
|
91
|
+
<button
|
|
92
|
+
type="button"
|
|
93
|
+
className={styles.bodyToggle}
|
|
94
|
+
onClick={() => { setExpanded((v) => !v); }}
|
|
95
|
+
aria-expanded={expanded}
|
|
96
|
+
data-testid="tool-card-toggle"
|
|
97
|
+
>
|
|
98
|
+
<span className={`${styles.chevron} ${expanded ? styles.chevronExpanded : ""}`} aria-hidden="true"><ChevronRight size={ICON_SM} /></span>
|
|
99
|
+
{expanded ? "collapse" : `${outputLines.length - PREVIEW_LINES} more lines`}
|
|
100
|
+
</button>
|
|
101
|
+
)}
|
|
102
|
+
</>
|
|
103
|
+
)}
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
}
|