@grackle-ai/web-components 0.115.2 → 0.116.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.rush/temp/chunked-rush-logs/web-components._phase_build.chunks.jsonl +6 -6
- package/.rush/temp/chunked-rush-logs/web-components._phase_test.chunks.jsonl +20 -21
- package/.rush/temp/{b32d9c7748f6c2c43df816a4bdd427ae0c7f1e32.tar.log → f238c174d295031bec7f186733732e0fd7e4b9a5.tar.log} +55 -60
- package/.rush/temp/{49e5384757b767ffca6c218faf139f6813911f4a.untar.log → f238c174d295031bec7f186733732e0fd7e4b9a5.untar.log} +2 -2
- package/.rush/temp/{49e5384757b767ffca6c218faf139f6813911f4a.tar.log → f5301e6a84109dcec06242d178df01e555a83456.tar.log} +2 -2
- package/.rush/temp/{b32d9c7748f6c2c43df816a4bdd427ae0c7f1e32.untar.log → f5301e6a84109dcec06242d178df01e555a83456.untar.log} +2 -2
- package/.rush/temp/operation/_phase_build/all.log +6 -6
- package/.rush/temp/operation/_phase_build/log-chunks.jsonl +6 -6
- package/.rush/temp/operation/_phase_build/state.json +1 -1
- package/.rush/temp/operation/_phase_test/all.log +20 -21
- package/.rush/temp/operation/_phase_test/log-chunks.jsonl +20 -21
- package/.rush/temp/operation/_phase_test/state.json +1 -1
- package/.rush/temp/shrinkwrap-deps.json +3 -3
- package/README.md +2 -2
- package/dist/index.css +1 -1
- package/dist/index.js +9055 -9498
- package/package.json +2 -2
- package/rush-logs/web-components._phase_build.cache.log +1 -1
- package/rush-logs/web-components._phase_build.log +6 -6
- package/rush-logs/web-components._phase_test.cache.log +1 -1
- package/rush-logs/web-components._phase_test.log +20 -21
- package/src/components/index.ts +0 -3
- package/src/components/knowledge/KnowledgeDetailPanel.tsx +1 -4
- package/src/components/layout/AppNav.stories.tsx +3 -6
- package/src/components/layout/AppNav.tsx +3 -7
- package/src/components/layout/BottomStatusBar.tsx +4 -6
- package/src/components/lists/index.ts +0 -1
- package/src/components/panels/KeyboardShortcutsPanel.tsx +0 -1
- package/src/components/panels/index.ts +0 -1
- package/src/components/personas/McpToolSelector.stories.tsx +12 -12
- package/src/components/tools/ToolCard.stories.tsx +0 -26
- package/src/components/tools/ToolCard.tsx +0 -3
- package/src/components/tools/ToolSearchCard.stories.tsx +8 -8
- package/src/components/tools/WorkpadCard.stories.tsx +5 -5
- package/src/components/tools/classifyTool.test.ts +0 -1
- package/src/components/tools/classifyTool.ts +2 -7
- package/src/context/GrackleContextTypes.ts +1 -3
- package/src/hooks/types.ts +1 -44
- package/src/index.ts +4 -8
- package/src/mocks/MockGrackleProvider.tsx +0 -75
- package/src/mocks/mockData.ts +8 -99
- package/src/test-utils/storybook-helpers.ts +0 -19
- package/src/utils/breadcrumbs.test.ts +0 -43
- package/src/utils/breadcrumbs.ts +1 -37
- package/src/utils/navigation.ts +1 -20
- package/src/utils/route-config.test.ts +0 -31
- package/temp/build/lint/_eslint-5eVG3S6w.json +30 -54
- package/src/components/lists/FindingsNav.module.scss +0 -126
- package/src/components/lists/FindingsNav.tsx +0 -146
- package/src/components/panels/FindingsPanel.module.scss +0 -94
- package/src/components/panels/FindingsPanel.stories.tsx +0 -109
- package/src/components/panels/FindingsPanel.tsx +0 -76
- package/src/components/tools/FindingCard.stories.tsx +0 -124
- package/src/components/tools/FindingCard.tsx +0 -178
- package/src/utils/findingCategory.ts +0 -33
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
-
import { expect } from "@storybook/test";
|
|
3
|
-
import { FindingsPanel } from "./FindingsPanel.js";
|
|
4
|
-
import { buildFinding } from "../../test-utils/storybook-helpers.js";
|
|
5
|
-
|
|
6
|
-
const meta: Meta<typeof FindingsPanel> = {
|
|
7
|
-
title: "Grackle/Panels/FindingsPanel",
|
|
8
|
-
component: FindingsPanel,
|
|
9
|
-
tags: ["autodocs"],
|
|
10
|
-
args: {
|
|
11
|
-
findings: [],
|
|
12
|
-
},
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export default meta;
|
|
16
|
-
type Story = StoryObj<typeof FindingsPanel>;
|
|
17
|
-
|
|
18
|
-
/** Empty state shows a placeholder message when there are no findings. */
|
|
19
|
-
export const EmptyState: Story = {
|
|
20
|
-
args: {
|
|
21
|
-
findings: [],
|
|
22
|
-
},
|
|
23
|
-
play: async ({ canvas }) => {
|
|
24
|
-
await expect(
|
|
25
|
-
canvas.getByText("No findings yet. Agents will post discoveries here."),
|
|
26
|
-
).toBeInTheDocument();
|
|
27
|
-
},
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
/** Renders a single finding card with category badge, title, content, and tags. */
|
|
31
|
-
export const SingleFinding: Story = {
|
|
32
|
-
args: {
|
|
33
|
-
findings: [
|
|
34
|
-
buildFinding({
|
|
35
|
-
id: "f-1",
|
|
36
|
-
category: "architecture",
|
|
37
|
-
title: "Service boundary issue",
|
|
38
|
-
content: "The auth service is tightly coupled to the user service.",
|
|
39
|
-
tags: ["coupling", "refactor"],
|
|
40
|
-
}),
|
|
41
|
-
],
|
|
42
|
-
},
|
|
43
|
-
play: async ({ canvas }) => {
|
|
44
|
-
await expect(canvas.getByText("architecture")).toBeInTheDocument();
|
|
45
|
-
await expect(canvas.getByText("Service boundary issue")).toBeInTheDocument();
|
|
46
|
-
await expect(canvas.getByText(/tightly coupled/)).toBeInTheDocument();
|
|
47
|
-
await expect(canvas.getByText("coupling")).toBeInTheDocument();
|
|
48
|
-
await expect(canvas.getByText("refactor")).toBeInTheDocument();
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
/** Renders multiple findings across different categories. */
|
|
53
|
-
export const MultipleFindings: Story = {
|
|
54
|
-
args: {
|
|
55
|
-
findings: [
|
|
56
|
-
buildFinding({
|
|
57
|
-
id: "f-1",
|
|
58
|
-
category: "bug",
|
|
59
|
-
title: "Race condition in session cleanup",
|
|
60
|
-
content: "When two sessions end simultaneously, the cleanup handler may skip one.",
|
|
61
|
-
tags: ["concurrency"],
|
|
62
|
-
}),
|
|
63
|
-
buildFinding({
|
|
64
|
-
id: "f-2",
|
|
65
|
-
category: "api",
|
|
66
|
-
title: "Missing pagination on list endpoints",
|
|
67
|
-
content: "The list_tasks endpoint returns all tasks without pagination support.",
|
|
68
|
-
tags: ["api", "performance"],
|
|
69
|
-
}),
|
|
70
|
-
buildFinding({
|
|
71
|
-
id: "f-3",
|
|
72
|
-
category: "decision",
|
|
73
|
-
title: "Chose SQLite over PostgreSQL",
|
|
74
|
-
content: "SQLite with WAL mode provides sufficient concurrency for single-server deployment.",
|
|
75
|
-
tags: ["database"],
|
|
76
|
-
}),
|
|
77
|
-
],
|
|
78
|
-
},
|
|
79
|
-
play: async ({ canvas }) => {
|
|
80
|
-
// Use getAllByText for "api" since it appears as both a category badge and a tag
|
|
81
|
-
await expect(canvas.getByText("bug")).toBeInTheDocument();
|
|
82
|
-
const apiElements = canvas.getAllByText("api");
|
|
83
|
-
await expect(apiElements.length).toBeGreaterThanOrEqual(1);
|
|
84
|
-
await expect(canvas.getByText("decision")).toBeInTheDocument();
|
|
85
|
-
await expect(canvas.getByText("Race condition in session cleanup")).toBeInTheDocument();
|
|
86
|
-
await expect(canvas.getByText("Missing pagination on list endpoints")).toBeInTheDocument();
|
|
87
|
-
await expect(canvas.getByText("Chose SQLite over PostgreSQL")).toBeInTheDocument();
|
|
88
|
-
},
|
|
89
|
-
};
|
|
90
|
-
|
|
91
|
-
/** Long content is truncated at 300 characters with an ellipsis. */
|
|
92
|
-
export const LongContentTruncated: Story = {
|
|
93
|
-
args: {
|
|
94
|
-
findings: [
|
|
95
|
-
buildFinding({
|
|
96
|
-
id: "f-long",
|
|
97
|
-
category: "pattern",
|
|
98
|
-
title: "Verbose finding",
|
|
99
|
-
content: "A".repeat(400),
|
|
100
|
-
tags: [],
|
|
101
|
-
}),
|
|
102
|
-
],
|
|
103
|
-
},
|
|
104
|
-
play: async ({ canvas }) => {
|
|
105
|
-
// The rendered content should end with "..." since it exceeds 300 chars
|
|
106
|
-
const contentEl = canvas.getByText(/A{10,}\.\.\.$/);
|
|
107
|
-
await expect(contentEl).toBeInTheDocument();
|
|
108
|
-
},
|
|
109
|
-
};
|
|
@@ -1,76 +0,0 @@
|
|
|
1
|
-
import type { JSX } from "react";
|
|
2
|
-
import { motion } from "motion/react";
|
|
3
|
-
import type { FindingData } from "../../hooks/types.js";
|
|
4
|
-
import styles from "./FindingsPanel.module.scss";
|
|
5
|
-
import { formatRelativeTime } from "../../utils/time.js";
|
|
6
|
-
import { getCategoryColor } from "../../utils/findingCategory.js";
|
|
7
|
-
|
|
8
|
-
/** Props for the FindingsPanel component. */
|
|
9
|
-
interface Props {
|
|
10
|
-
/** Pre-filtered findings to display. */
|
|
11
|
-
findings: FindingData[];
|
|
12
|
-
/** Optional click handler for finding cards. When provided, cards become clickable. */
|
|
13
|
-
onFindingClick?: (findingId: string) => void;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** Displays workspace findings as styled cards with staggered entrance animation. */
|
|
17
|
-
export function FindingsPanel({ findings, onFindingClick }: Props): JSX.Element {
|
|
18
|
-
if (findings.length === 0) {
|
|
19
|
-
return (
|
|
20
|
-
<div className={styles.emptyState}>
|
|
21
|
-
No findings yet. Agents will post discoveries here.
|
|
22
|
-
</div>
|
|
23
|
-
);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
return (
|
|
27
|
-
<div className={styles.container}>
|
|
28
|
-
{findings.map((f, index) => {
|
|
29
|
-
const categoryColor = getCategoryColor(f.category);
|
|
30
|
-
const Tag = onFindingClick ? motion.button : motion.div;
|
|
31
|
-
return (
|
|
32
|
-
<Tag
|
|
33
|
-
key={f.id}
|
|
34
|
-
type={onFindingClick ? "button" : undefined}
|
|
35
|
-
className={`${styles.card} ${onFindingClick ? styles.cardClickable : ""}`}
|
|
36
|
-
initial={{ opacity: 0, y: 8 }}
|
|
37
|
-
animate={{ opacity: 1, y: 0 }}
|
|
38
|
-
transition={{ delay: index * 0.05, duration: 0.2 }}
|
|
39
|
-
onClick={onFindingClick ? () => { onFindingClick(f.id); } : undefined}
|
|
40
|
-
>
|
|
41
|
-
<div className={styles.cardHeader}>
|
|
42
|
-
<span
|
|
43
|
-
className={styles.categoryBadge}
|
|
44
|
-
style={{ background: categoryColor.bg, color: categoryColor.text }}
|
|
45
|
-
>
|
|
46
|
-
{f.category}
|
|
47
|
-
</span>
|
|
48
|
-
<span className={styles.findingTitle}>
|
|
49
|
-
{f.title}
|
|
50
|
-
</span>
|
|
51
|
-
<span className={styles.findingDate} title={f.createdAt}>
|
|
52
|
-
{formatRelativeTime(f.createdAt)}
|
|
53
|
-
</span>
|
|
54
|
-
</div>
|
|
55
|
-
<div className={styles.findingContent}>
|
|
56
|
-
{f.content.length > 300 ? f.content.slice(0, 300) + "..." : f.content}
|
|
57
|
-
</div>
|
|
58
|
-
{f.tags.length > 0 && (
|
|
59
|
-
<div className={styles.tags}>
|
|
60
|
-
{f.tags.map((tag) => (
|
|
61
|
-
<span
|
|
62
|
-
key={tag}
|
|
63
|
-
className={styles.tag}
|
|
64
|
-
style={{ color: categoryColor.text, textShadow: `0 0 8px ${categoryColor.text}` }}
|
|
65
|
-
>
|
|
66
|
-
{tag}
|
|
67
|
-
</span>
|
|
68
|
-
))}
|
|
69
|
-
</div>
|
|
70
|
-
)}
|
|
71
|
-
</Tag>
|
|
72
|
-
);
|
|
73
|
-
})}
|
|
74
|
-
</div>
|
|
75
|
-
);
|
|
76
|
-
}
|
|
@@ -1,124 +0,0 @@
|
|
|
1
|
-
import type { Meta, StoryObj } from "@storybook/react";
|
|
2
|
-
import { expect } from "@storybook/test";
|
|
3
|
-
import { FindingCard } from "./FindingCard.js";
|
|
4
|
-
|
|
5
|
-
const meta: Meta<typeof FindingCard> = {
|
|
6
|
-
component: FindingCard,
|
|
7
|
-
title: "Tools/FindingCard",
|
|
8
|
-
};
|
|
9
|
-
export default meta;
|
|
10
|
-
type Story = StoryObj<typeof FindingCard>;
|
|
11
|
-
|
|
12
|
-
export const PostInProgress: Story = {
|
|
13
|
-
name: "finding_post - in progress",
|
|
14
|
-
args: {
|
|
15
|
-
tool: "mcp__grackle__finding_post",
|
|
16
|
-
args: {
|
|
17
|
-
title: "Auth middleware stores tokens insecurely",
|
|
18
|
-
category: "bug",
|
|
19
|
-
tags: ["security", "auth"],
|
|
20
|
-
},
|
|
21
|
-
},
|
|
22
|
-
play: async ({ canvas }) => {
|
|
23
|
-
await expect(canvas.getByTestId("tool-card-finding")).toBeInTheDocument();
|
|
24
|
-
await expect(canvas.getByTestId("tool-card-finding-title")).toHaveTextContent("Auth middleware");
|
|
25
|
-
await expect(canvas.getByTestId("tool-card-finding-category")).toHaveTextContent("bug");
|
|
26
|
-
},
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export const PostCompleted: Story = {
|
|
30
|
-
name: "finding_post - completed",
|
|
31
|
-
args: {
|
|
32
|
-
tool: "mcp__grackle__finding_post",
|
|
33
|
-
args: {
|
|
34
|
-
title: "Qdrant catalog naming convention",
|
|
35
|
-
category: "insight",
|
|
36
|
-
tags: ["search", "worktree", "qdrant"],
|
|
37
|
-
},
|
|
38
|
-
result: JSON.stringify({
|
|
39
|
-
id: "589f1e83",
|
|
40
|
-
workspaceId: "default",
|
|
41
|
-
category: "insight",
|
|
42
|
-
title: "Qdrant catalog naming convention",
|
|
43
|
-
content: "The qdrant-search MCP server indexes the codebase under the catalog name \"grackle\". All worktrees share the same codebase, so every semantic search call must pass catalog: \"grackle\".",
|
|
44
|
-
tags: ["search", "worktree", "qdrant"],
|
|
45
|
-
createdAt: "2026-03-28 03:49:15",
|
|
46
|
-
}),
|
|
47
|
-
},
|
|
48
|
-
play: async ({ canvas }) => {
|
|
49
|
-
await expect(canvas.getByTestId("tool-card-finding")).toBeInTheDocument();
|
|
50
|
-
await expect(canvas.getByTestId("tool-card-finding-category")).toHaveTextContent("insight");
|
|
51
|
-
await expect(canvas.getByTestId("tool-card-finding-tags")).toBeInTheDocument();
|
|
52
|
-
await expect(canvas.getByTestId("tool-card-finding-content")).toBeInTheDocument();
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
export const PostCopilotFormat: Story = {
|
|
57
|
-
name: "finding_post - Copilot tool name",
|
|
58
|
-
args: {
|
|
59
|
-
tool: "grackle-finding_post",
|
|
60
|
-
args: {
|
|
61
|
-
title: "Rush worktree usage",
|
|
62
|
-
category: "insight",
|
|
63
|
-
tags: ["workflow"],
|
|
64
|
-
},
|
|
65
|
-
result: JSON.stringify({
|
|
66
|
-
id: "e7091ea6",
|
|
67
|
-
category: "insight",
|
|
68
|
-
title: "Rush worktree usage",
|
|
69
|
-
content: "This codebase uses Rush worktrees for all feature development.",
|
|
70
|
-
tags: ["workflow"],
|
|
71
|
-
createdAt: "2026-03-28 03:53:07",
|
|
72
|
-
}),
|
|
73
|
-
},
|
|
74
|
-
play: async ({ canvas }) => {
|
|
75
|
-
await expect(canvas.getByTestId("tool-card-finding")).toBeInTheDocument();
|
|
76
|
-
// Should show bare tool name, not the full prefixed name
|
|
77
|
-
await expect(canvas.getByText("finding_post")).toBeInTheDocument();
|
|
78
|
-
},
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
export const ListWithResults: Story = {
|
|
82
|
-
name: "finding_list - multiple results",
|
|
83
|
-
args: {
|
|
84
|
-
tool: "mcp__grackle__finding_list",
|
|
85
|
-
args: { limit: 20 },
|
|
86
|
-
result: JSON.stringify([
|
|
87
|
-
{ id: "f1", category: "insight", title: "Qdrant catalog naming" },
|
|
88
|
-
{ id: "f2", category: "bug", title: "Auth token storage issue" },
|
|
89
|
-
{ id: "f3", category: "decision", title: "Use ConnectRPC over ws-bridge" },
|
|
90
|
-
]),
|
|
91
|
-
},
|
|
92
|
-
play: async ({ canvas }) => {
|
|
93
|
-
await expect(canvas.getByTestId("tool-card-finding")).toBeInTheDocument();
|
|
94
|
-
await expect(canvas.getByTestId("tool-card-finding-count")).toHaveTextContent("3 findings");
|
|
95
|
-
await expect(canvas.getByTestId("tool-card-finding-list")).toBeInTheDocument();
|
|
96
|
-
},
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
export const ListEmpty: Story = {
|
|
100
|
-
name: "finding_list - no results",
|
|
101
|
-
args: {
|
|
102
|
-
tool: "mcp__grackle__finding_list",
|
|
103
|
-
args: { category: "bug" },
|
|
104
|
-
result: JSON.stringify([]),
|
|
105
|
-
},
|
|
106
|
-
play: async ({ canvas }) => {
|
|
107
|
-
await expect(canvas.getByTestId("tool-card-finding")).toBeInTheDocument();
|
|
108
|
-
await expect(canvas.getByTestId("tool-card-finding-count")).toHaveTextContent("0 findings");
|
|
109
|
-
},
|
|
110
|
-
};
|
|
111
|
-
|
|
112
|
-
export const ErrorState: Story = {
|
|
113
|
-
name: "finding_post - error",
|
|
114
|
-
args: {
|
|
115
|
-
tool: "mcp__grackle__finding_post",
|
|
116
|
-
args: { title: "Test finding" },
|
|
117
|
-
result: "gRPC error [Internal]: database connection failed",
|
|
118
|
-
isError: true,
|
|
119
|
-
},
|
|
120
|
-
play: async ({ canvas }) => {
|
|
121
|
-
await expect(canvas.getByTestId("tool-card-finding")).toBeInTheDocument();
|
|
122
|
-
await expect(canvas.getByTestId("tool-card-error")).toBeInTheDocument();
|
|
123
|
-
},
|
|
124
|
-
};
|
|
@@ -1,178 +0,0 @@
|
|
|
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 single finding in MCP results. */
|
|
7
|
-
interface Finding {
|
|
8
|
-
id?: string;
|
|
9
|
-
title?: string;
|
|
10
|
-
category?: string;
|
|
11
|
-
content?: string;
|
|
12
|
-
tags?: string[];
|
|
13
|
-
createdAt?: string;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
/** Extracts finding-relevant fields from tool args. */
|
|
17
|
-
function getArgs(args: unknown): { title?: string; category?: string; tags?: string[] } {
|
|
18
|
-
if (args === null || args === undefined || typeof args !== "object") {
|
|
19
|
-
return {};
|
|
20
|
-
}
|
|
21
|
-
const a = args as Record<string, unknown>;
|
|
22
|
-
return {
|
|
23
|
-
title: typeof a.title === "string" ? a.title : undefined,
|
|
24
|
-
category: typeof a.category === "string" ? a.category : undefined,
|
|
25
|
-
tags: Array.isArray(a.tags) ? (a.tags as string[]) : undefined,
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Parses MCP result JSON into a finding or array of findings. */
|
|
30
|
-
function parseResult(result: string | undefined): { single?: Finding; list?: Finding[] } {
|
|
31
|
-
if (!result) {
|
|
32
|
-
return {};
|
|
33
|
-
}
|
|
34
|
-
try {
|
|
35
|
-
const parsed: unknown = JSON.parse(result);
|
|
36
|
-
if (Array.isArray(parsed)) {
|
|
37
|
-
return { list: (parsed as unknown[]).filter((v): v is Finding => v !== null && typeof v === "object") };
|
|
38
|
-
}
|
|
39
|
-
if (typeof parsed === "object" && parsed !== null) {
|
|
40
|
-
return { single: parsed as Finding };
|
|
41
|
-
}
|
|
42
|
-
} catch { /* fall through */ }
|
|
43
|
-
return {};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/** Number of items shown when collapsed. */
|
|
47
|
-
const PREVIEW_COUNT: number = 5;
|
|
48
|
-
|
|
49
|
-
/** Number of content lines shown when collapsed. */
|
|
50
|
-
const PREVIEW_LINES: number = 5;
|
|
51
|
-
|
|
52
|
-
/** Renders a finding tool call (finding_post, finding_list) with structured display. */
|
|
53
|
-
export function FindingCard({ tool, args, result, isError }: ToolCardProps): JSX.Element {
|
|
54
|
-
const [expanded, setExpanded] = useState(false);
|
|
55
|
-
const bareName = extractBareName(tool);
|
|
56
|
-
const argData = getArgs(args);
|
|
57
|
-
const inProgress = result === undefined;
|
|
58
|
-
const { single, list } = parseResult(result);
|
|
59
|
-
|
|
60
|
-
// Determine title to show in header
|
|
61
|
-
const displayTitle = single?.title ?? argData.title;
|
|
62
|
-
// Only show category badge for single findings, not when displaying a list
|
|
63
|
-
const displayCategory = list ? undefined : (single?.category ?? argData.category);
|
|
64
|
-
const displayTags = list ? undefined : (single?.tags ?? argData.tags);
|
|
65
|
-
|
|
66
|
-
return (
|
|
67
|
-
<div
|
|
68
|
-
className={`${styles.card} ${isError ? styles.cardRed : styles.cardPurple} ${inProgress ? styles.inProgress : ""}`}
|
|
69
|
-
data-testid="tool-card-finding"
|
|
70
|
-
>
|
|
71
|
-
<div className={styles.header}>
|
|
72
|
-
<span className={styles.icon} aria-hidden="true">💡</span>
|
|
73
|
-
<span className={styles.toolName} style={{ color: "var(--accent-purple, #a78bfa)" }}>
|
|
74
|
-
{bareName}
|
|
75
|
-
</span>
|
|
76
|
-
{displayTitle && (
|
|
77
|
-
<span className={styles.fileName} data-testid="tool-card-finding-title">
|
|
78
|
-
"{displayTitle}"
|
|
79
|
-
</span>
|
|
80
|
-
)}
|
|
81
|
-
{displayCategory && (
|
|
82
|
-
<>
|
|
83
|
-
<span className={styles.spacer} />
|
|
84
|
-
<span className={styles.badge} data-testid="tool-card-finding-category">
|
|
85
|
-
{displayCategory}
|
|
86
|
-
</span>
|
|
87
|
-
</>
|
|
88
|
-
)}
|
|
89
|
-
{list && !displayCategory && (
|
|
90
|
-
<>
|
|
91
|
-
<span className={styles.spacer} />
|
|
92
|
-
<span className={styles.badge} data-testid="tool-card-finding-count">
|
|
93
|
-
{list.length} {list.length === 1 ? "finding" : "findings"}
|
|
94
|
-
</span>
|
|
95
|
-
</>
|
|
96
|
-
)}
|
|
97
|
-
</div>
|
|
98
|
-
|
|
99
|
-
{/* Tags */}
|
|
100
|
-
{displayTags && displayTags.length > 0 && (
|
|
101
|
-
<div className={styles.pre} style={{ padding: "4px 8px", whiteSpace: "normal" }} data-testid="tool-card-finding-tags">
|
|
102
|
-
{displayTags.map((tag, i) => (
|
|
103
|
-
<span key={i} style={{ display: "inline-block", marginRight: "6px", opacity: 0.7, fontSize: "0.85em" }}>
|
|
104
|
-
#{tag}
|
|
105
|
-
</span>
|
|
106
|
-
))}
|
|
107
|
-
</div>
|
|
108
|
-
)}
|
|
109
|
-
|
|
110
|
-
{/* In-progress: show args summary */}
|
|
111
|
-
{inProgress && !displayTitle && args !== null && args !== undefined && (
|
|
112
|
-
<pre className={styles.pre} data-testid="tool-card-args">
|
|
113
|
-
{JSON.stringify(args, null, 2)}
|
|
114
|
-
</pre>
|
|
115
|
-
)}
|
|
116
|
-
|
|
117
|
-
{/* Error */}
|
|
118
|
-
{isError && result && (
|
|
119
|
-
<pre className={styles.pre} data-testid="tool-card-error">
|
|
120
|
-
{result}
|
|
121
|
-
</pre>
|
|
122
|
-
)}
|
|
123
|
-
|
|
124
|
-
{/* Single finding result: show content */}
|
|
125
|
-
{!isError && single?.content && (
|
|
126
|
-
<>
|
|
127
|
-
{(() => {
|
|
128
|
-
const lines = single.content.split("\n");
|
|
129
|
-
const hasMore = lines.length > PREVIEW_LINES;
|
|
130
|
-
const displayContent = expanded ? single.content : lines.slice(0, PREVIEW_LINES).join("\n");
|
|
131
|
-
return (
|
|
132
|
-
<>
|
|
133
|
-
<pre className={styles.pre} data-testid="tool-card-finding-content">
|
|
134
|
-
{displayContent}
|
|
135
|
-
</pre>
|
|
136
|
-
{hasMore && (
|
|
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" : `${lines.length - PREVIEW_LINES} more lines`}
|
|
146
|
-
</button>
|
|
147
|
-
)}
|
|
148
|
-
</>
|
|
149
|
-
);
|
|
150
|
-
})()}
|
|
151
|
-
</>
|
|
152
|
-
)}
|
|
153
|
-
|
|
154
|
-
{/* List result: show compact finding titles */}
|
|
155
|
-
{!isError && list && list.length > 0 && (
|
|
156
|
-
<>
|
|
157
|
-
<pre className={styles.pre} data-testid="tool-card-finding-list">
|
|
158
|
-
{(expanded ? list : list.slice(0, PREVIEW_COUNT)).map((f, i) => (
|
|
159
|
-
`${f.category ? `[${f.category}] ` : ""}${f.title ?? f.id ?? `Finding ${i + 1}`}`
|
|
160
|
-
)).join("\n")}
|
|
161
|
-
</pre>
|
|
162
|
-
{list.length > PREVIEW_COUNT && (
|
|
163
|
-
<button
|
|
164
|
-
type="button"
|
|
165
|
-
className={styles.bodyToggle}
|
|
166
|
-
onClick={() => { setExpanded((v) => !v); }}
|
|
167
|
-
aria-expanded={expanded}
|
|
168
|
-
data-testid="tool-card-toggle"
|
|
169
|
-
>
|
|
170
|
-
<span className={`${styles.chevron} ${expanded ? styles.chevronExpanded : ""}`}>▸</span>
|
|
171
|
-
{expanded ? "collapse" : `${list.length - PREVIEW_COUNT} more findings`}
|
|
172
|
-
</button>
|
|
173
|
-
)}
|
|
174
|
-
</>
|
|
175
|
-
)}
|
|
176
|
-
</div>
|
|
177
|
-
);
|
|
178
|
-
}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Shared category-to-color mapping for findings.
|
|
3
|
-
*
|
|
4
|
-
* Centralized here so FindingsPanel, FindingsNav, and consuming pages
|
|
5
|
-
* all use the same palette and stay in sync.
|
|
6
|
-
*
|
|
7
|
-
* @module
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
/** Color pair for a finding category. */
|
|
11
|
-
export interface CategoryColor {
|
|
12
|
-
/** Foreground / text color (CSS custom property). */
|
|
13
|
-
text: string;
|
|
14
|
-
/** Background / badge color (CSS custom property). */
|
|
15
|
-
bg: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
/** Category color mapping using CSS custom property values. */
|
|
19
|
-
export const CATEGORY_COLORS: Record<string, CategoryColor> = {
|
|
20
|
-
architecture: { text: "var(--accent-blue)", bg: "var(--accent-blue-dim)" },
|
|
21
|
-
api: { text: "var(--accent-green)", bg: "var(--accent-green-dim)" },
|
|
22
|
-
bug: { text: "var(--accent-red)", bg: "var(--accent-red-dim)" },
|
|
23
|
-
decision: { text: "var(--accent-yellow)", bg: "var(--accent-yellow-dim)" },
|
|
24
|
-
dependency: { text: "var(--accent-purple)", bg: "var(--accent-purple-dim)" },
|
|
25
|
-
pattern: { text: "var(--accent-cyan)", bg: "var(--accent-cyan-dim)" },
|
|
26
|
-
general: { text: "var(--text-secondary)", bg: "var(--bg-elevated)" },
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
/** Look up the color pair for a category, falling back to `general`. */
|
|
30
|
-
export function getCategoryColor(category: string): CategoryColor {
|
|
31
|
-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition -- category may not be in the map
|
|
32
|
-
return CATEGORY_COLORS[category] || CATEGORY_COLORS.general;
|
|
33
|
-
}
|