@iloveagents/foundry-web-ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/AGENTS.md +59 -0
  2. package/CLAUDE.md +1 -0
  3. package/LICENSE +21 -0
  4. package/README.md +41 -0
  5. package/package.json +61 -0
  6. package/src/__tests__/no-spaces-imports.test.ts +59 -0
  7. package/src/__tests__/open-core-guards.test.ts +307 -0
  8. package/src/__tests__/theme-runtime.test.ts +81 -0
  9. package/src/__tests__/tool-fallback.test.tsx +109 -0
  10. package/src/components/ag-ui-runtime-provider.tsx +59 -0
  11. package/src/components/app-brand.tsx +38 -0
  12. package/src/components/assistant-chat.tsx +423 -0
  13. package/src/components/chat-attachments.tsx +68 -0
  14. package/src/components/chat-bubble.tsx +339 -0
  15. package/src/components/chat-header.tsx +44 -0
  16. package/src/components/client-tool-executor.tsx +230 -0
  17. package/src/components/collection-empty-state.tsx +37 -0
  18. package/src/components/collection-filter-bar.tsx +168 -0
  19. package/src/components/collection-search-input.tsx +41 -0
  20. package/src/components/collection-skeleton.tsx +55 -0
  21. package/src/components/collection-sort-menu.tsx +62 -0
  22. package/src/components/collection-surface.tsx +46 -0
  23. package/src/components/collection-toolbar.tsx +33 -0
  24. package/src/components/collection-view-toggle.tsx +61 -0
  25. package/src/components/confirm-dialog.tsx +49 -0
  26. package/src/components/confirmation-card.tsx +32 -0
  27. package/src/components/context-badges.tsx +124 -0
  28. package/src/components/context-bar.tsx +202 -0
  29. package/src/components/form-dialog.tsx +56 -0
  30. package/src/components/global-selection-popover.tsx +135 -0
  31. package/src/components/infinite-scroll-sentinel.tsx +48 -0
  32. package/src/components/json-viewer.tsx +232 -0
  33. package/src/components/loading-indicator.tsx +40 -0
  34. package/src/components/markdown-text.tsx +379 -0
  35. package/src/components/name-dialog.tsx +87 -0
  36. package/src/components/selection-popover.tsx +156 -0
  37. package/src/components/show-document-tool-ui.tsx +90 -0
  38. package/src/components/sidebar.test.tsx +182 -0
  39. package/src/components/sidebar.tsx +1174 -0
  40. package/src/components/surface-card.tsx +45 -0
  41. package/src/components/theme-runtime-provider.tsx +93 -0
  42. package/src/components/theme-toggle.tsx +27 -0
  43. package/src/components/tool-call-card.tsx +126 -0
  44. package/src/components/tool-fallback.tsx +169 -0
  45. package/src/components/tool-panel-layout.tsx +36 -0
  46. package/src/components/tool-panel.tsx +233 -0
  47. package/src/components/tooltip-icon-button.tsx +28 -0
  48. package/src/components/user-menu.tsx +60 -0
  49. package/src/index.ts +170 -0
  50. package/src/lib/__tests__/ag-ui-adapter.test.ts +182 -0
  51. package/src/lib/__tests__/app-store.test.ts +330 -0
  52. package/src/lib/__tests__/attachment-adapter.test.ts +48 -0
  53. package/src/lib/__tests__/chat-bubble-store.test.ts +67 -0
  54. package/src/lib/__tests__/client-tools.test.ts +124 -0
  55. package/src/lib/__tests__/dev-store.test.ts +78 -0
  56. package/src/lib/__tests__/nav-config.test.ts +135 -0
  57. package/src/lib/__tests__/nav-dnd.test.ts +24 -0
  58. package/src/lib/__tests__/nav-store.test.ts +59 -0
  59. package/src/lib/__tests__/sidebar-store.test.ts +144 -0
  60. package/src/lib/__tests__/theme-store.test.ts +83 -0
  61. package/src/lib/__tests__/tool-panel-store.test.ts +61 -0
  62. package/src/lib/__tests__/use-page-context.test.ts +58 -0
  63. package/src/lib/ag-ui-adapter.ts +362 -0
  64. package/src/lib/app-store.ts +294 -0
  65. package/src/lib/attachment-adapter.ts +92 -0
  66. package/src/lib/auth-provider.tsx +144 -0
  67. package/src/lib/chat-bubble-store.ts +46 -0
  68. package/src/lib/client-tools.ts +293 -0
  69. package/src/lib/dev-store.ts +69 -0
  70. package/src/lib/nav-config.ts +292 -0
  71. package/src/lib/nav-dnd.ts +14 -0
  72. package/src/lib/nav-store.ts +76 -0
  73. package/src/lib/sidebar-store.ts +113 -0
  74. package/src/lib/theme-runtime.ts +660 -0
  75. package/src/lib/theme-store.ts +67 -0
  76. package/src/lib/tool-panel-store.ts +121 -0
  77. package/src/lib/use-new-conversation.test.tsx +124 -0
  78. package/src/lib/use-new-conversation.ts +33 -0
  79. package/src/lib/use-page-tools.ts +40 -0
  80. package/src/ui/collapsible.tsx +7 -0
  81. package/src/ui/dialog.tsx +109 -0
  82. package/src/ui/dropdown-menu.tsx +71 -0
  83. package/src/ui/popover.tsx +34 -0
  84. package/src/ui/select.tsx +15 -0
  85. package/src/ui/tooltip.tsx +30 -0
  86. package/src/vite-env.d.ts +1 -0
  87. package/tsconfig.json +16 -0
  88. package/vitest.config.ts +8 -0
@@ -0,0 +1,90 @@
1
+ import { makeAssistantToolUI } from "@assistant-ui/react";
2
+ import { useRef } from "react";
3
+ import { FileText } from "lucide-react";
4
+ import { useToolPanelStore } from "../lib/tool-panel-store.ts";
5
+ import { ToolCallCard } from "./tool-call-card.tsx";
6
+
7
+ interface ShowDocumentArgs {
8
+ title: string;
9
+ content: string;
10
+ type?: string;
11
+ }
12
+
13
+ interface ShowDocumentResult {
14
+ status?: "success" | "error";
15
+ message?: string;
16
+ }
17
+
18
+ export const ShowDocumentToolUI = makeAssistantToolUI<ShowDocumentArgs, ShowDocumentResult>({
19
+ toolName: "show_document",
20
+ render: ({ args, result, status, toolCallId }) => {
21
+ const openPanel = useToolPanelStore((state) => state.openPanel);
22
+ const openedToolCallsRef = useRef(new Set<string>());
23
+
24
+ if (
25
+ status.type === "complete" &&
26
+ result?.status === "success" &&
27
+ args &&
28
+ !openedToolCallsRef.current.has(toolCallId)
29
+ ) {
30
+ openedToolCallsRef.current.add(toolCallId);
31
+ queueMicrotask(() => {
32
+ openPanel({
33
+ title: args.title,
34
+ content: args.content,
35
+ type: (args.type as "markdown" | "text" | "page" | "pdf") || "markdown",
36
+ });
37
+ });
38
+ }
39
+
40
+ let description = "";
41
+ if (status.type === "running") {
42
+ description = "Opening document...";
43
+ } else if (status.type === "complete" && result) {
44
+ description =
45
+ result.message ||
46
+ (result.status === "success" ? "Document ready" : "Failed to open document");
47
+ } else if (status.type === "requires-action") {
48
+ description = "Action required to display document";
49
+ }
50
+
51
+ const handleViewDocument = () => {
52
+ if (args) {
53
+ openPanel({
54
+ title: args.title,
55
+ content: args.content,
56
+ type: (args.type as "markdown" | "text" | "page" | "pdf") || "markdown",
57
+ });
58
+ }
59
+ };
60
+
61
+ return (
62
+ <ToolCallCard
63
+ icon={<FileText className="size-3.5" />}
64
+ title="Document Display"
65
+ description={description}
66
+ status={status}
67
+ className="w-full"
68
+ action={
69
+ status.type === "complete" && result?.status === "success" && args
70
+ ? {
71
+ label: "View Document",
72
+ onClick: handleViewDocument,
73
+ }
74
+ : undefined
75
+ }
76
+ >
77
+ {args && (
78
+ <div className="text-muted-foreground text-xs">
79
+ <span className="font-medium">Title:</span> {args.title}
80
+ {args.type && (
81
+ <span className="ml-2">
82
+ <span className="font-medium">Type:</span> {args.type}
83
+ </span>
84
+ )}
85
+ </div>
86
+ )}
87
+ </ToolCallCard>
88
+ );
89
+ },
90
+ });
@@ -0,0 +1,182 @@
1
+ import { act } from "react";
2
+ import { createRoot, type Root } from "react-dom/client";
3
+ import { MemoryRouter } from "react-router";
4
+ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
5
+ import { FolderOpen, MessageSquare } from "lucide-react";
6
+ import { useNavStore } from "../lib/nav-store.ts";
7
+ import { useAppStore } from "../lib/app-store.ts";
8
+ import { SIDEBAR_WIDTH, useSidebarStore } from "../lib/sidebar-store.ts";
9
+ import type { NavGroup } from "../lib/nav-config.ts";
10
+ import { TooltipProvider } from "../ui/tooltip.tsx";
11
+
12
+ vi.mock("../lib/use-new-conversation.ts", () => ({
13
+ useNewConversation: () => () => {},
14
+ }));
15
+
16
+ describe("Sidebar active state", () => {
17
+ let container: HTMLDivElement;
18
+ let root: Root;
19
+ let originalActEnvironment: boolean | undefined;
20
+ let originalInnerWidth: number;
21
+ let originalMatchMedia: typeof window.matchMedia | undefined;
22
+
23
+ beforeEach(() => {
24
+ originalActEnvironment = globalThis.IS_REACT_ACT_ENVIRONMENT;
25
+ originalInnerWidth = window.innerWidth;
26
+ originalMatchMedia = window.matchMedia;
27
+ globalThis.IS_REACT_ACT_ENVIRONMENT = true;
28
+ localStorage.clear();
29
+ window.innerWidth = 1280;
30
+ Object.defineProperty(window, "matchMedia", {
31
+ configurable: true,
32
+ writable: true,
33
+ value:
34
+ originalMatchMedia ||
35
+ ((query: string) =>
36
+ ({
37
+ matches: false,
38
+ media: query,
39
+ onchange: null,
40
+ addListener: () => {},
41
+ removeListener: () => {},
42
+ addEventListener: () => {},
43
+ removeEventListener: () => {},
44
+ dispatchEvent: () => false,
45
+ }) as MediaQueryList),
46
+ });
47
+
48
+ const navConfig: NavGroup[] = [
49
+ {
50
+ label: "Pages",
51
+ hideLabel: true,
52
+ items: [
53
+ { to: "/", label: "Chat", icon: MessageSquare },
54
+ { to: "/spaces/demo", label: "Demo Workspace", icon: FolderOpen },
55
+ ],
56
+ },
57
+ ];
58
+
59
+ useNavStore.getState().setConfig(navConfig);
60
+ useNavStore.setState({ footerActions: [] });
61
+ useAppStore.setState({
62
+ currentPage: "/spaces/demo",
63
+ threadActive: false,
64
+ contextItems: [],
65
+ sentContext: {},
66
+ highlights: [],
67
+ selectedItemId: null,
68
+ content: {},
69
+ });
70
+ useSidebarStore.setState({ isOpen: true, isMobileOpen: false, width: SIDEBAR_WIDTH });
71
+
72
+ container = document.createElement("div");
73
+ document.body.appendChild(container);
74
+ root = createRoot(container);
75
+ });
76
+
77
+ afterEach(() => {
78
+ act(() => {
79
+ root.unmount();
80
+ });
81
+ container.remove();
82
+ globalThis.IS_REACT_ACT_ENVIRONMENT = originalActEnvironment;
83
+ window.innerWidth = originalInnerWidth;
84
+ if (originalMatchMedia) {
85
+ Object.defineProperty(window, "matchMedia", {
86
+ configurable: true,
87
+ writable: true,
88
+ value: originalMatchMedia,
89
+ });
90
+ } else {
91
+ // jsdom doesn't define this by default; remove our temporary override.
92
+ // @ts-expect-error deleting configurable test override
93
+ delete window.matchMedia;
94
+ }
95
+ });
96
+
97
+ it("uses the neutral selected-row styling for the current nav item", async () => {
98
+ const { Sidebar } = await import("./sidebar.tsx");
99
+
100
+ await act(async () => {
101
+ root.render(
102
+ <MemoryRouter initialEntries={["/spaces/demo"]}>
103
+ <TooltipProvider>
104
+ <Sidebar />
105
+ </TooltipProvider>
106
+ </MemoryRouter>,
107
+ );
108
+ });
109
+
110
+ const activeRow = Array.from(container.querySelectorAll<HTMLElement>("[class]")).find(
111
+ (element) =>
112
+ element.textContent?.includes("Demo Workspace") &&
113
+ element.className.includes("bg-sidebar-selected"),
114
+ );
115
+
116
+ expect(activeRow).toBeTruthy();
117
+ expect(activeRow?.className).toContain("bg-sidebar-selected");
118
+ expect(activeRow?.className).not.toContain("bg-sidebar-primary/10");
119
+ });
120
+
121
+ it("renders first-level child leaves and folders at the same indent depth", async () => {
122
+ const { Sidebar } = await import("./sidebar.tsx");
123
+
124
+ const navConfig: NavGroup[] = [
125
+ {
126
+ label: "Workspaces",
127
+ items: [
128
+ {
129
+ to: "/spaces/demo",
130
+ label: "Demo Workspace",
131
+ icon: FolderOpen,
132
+ children: [
133
+ { to: "/spaces/demo/documents", label: "Documents", icon: FolderOpen },
134
+ {
135
+ to: "/spaces/demo/views",
136
+ label: "Views",
137
+ icon: FolderOpen,
138
+ children: [
139
+ { to: "/spaces/demo/views/current", label: "Current", icon: FolderOpen },
140
+ ],
141
+ },
142
+ ],
143
+ },
144
+ ],
145
+ },
146
+ ];
147
+
148
+ useNavStore.getState().setConfig(navConfig);
149
+ useAppStore.setState({
150
+ currentPage: "/spaces/demo/documents",
151
+ threadActive: false,
152
+ contextItems: [],
153
+ sentContext: {},
154
+ highlights: [],
155
+ selectedItemId: null,
156
+ content: {},
157
+ });
158
+
159
+ await act(async () => {
160
+ root.render(
161
+ <MemoryRouter initialEntries={["/spaces/demo/documents"]}>
162
+ <TooltipProvider>
163
+ <Sidebar />
164
+ </TooltipProvider>
165
+ </MemoryRouter>,
166
+ );
167
+ });
168
+
169
+ const documentsButton = Array.from(container.querySelectorAll("button")).find((element) =>
170
+ element.textContent?.includes("Documents"),
171
+ );
172
+ const viewsButton = Array.from(container.querySelectorAll("button")).find((element) =>
173
+ element.textContent?.includes("Views"),
174
+ );
175
+
176
+ expect(documentsButton).toBeTruthy();
177
+ expect(viewsButton).toBeTruthy();
178
+ expect(documentsButton?.className).toContain("pl-7");
179
+ expect(viewsButton?.className).toContain("pl-7");
180
+ expect(documentsButton?.className).not.toContain("pl-5");
181
+ });
182
+ });