refactor(ui,hooks): extract shared ChatView and DevicePairing to packages
- Extract ChatView from web chat-page into packages/ui as a prop-driven component (accepts UseChatReturn shape, no transport dependency) - Move DevicePairing from apps/web to packages/ui with locally defined ConnectionIdentity type (no @multica/hooks dependency) - Create @multica/hooks package with useGatewayConnection and useChat (moved from apps/web/hooks) - Add isLoadingHistory state to useChat with skeleton loading in ChatView - Add MulticaIcon (pure CSS asterisk via clip-path, adapts to theme) - Slim web chat-page.tsx from 188 to 65 lines (just wires hooks to UI) Desktop can now reuse ChatView and DevicePairing directly. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
7fdbf24c4e
commit
53c350ea33
12 changed files with 316 additions and 89 deletions
|
|
@ -1,17 +1,11 @@
|
|||
"use client";
|
||||
|
||||
import { useRef } from "react";
|
||||
import { Header } from "@/app/header";
|
||||
import { Button } from "@multica/ui/components/ui/button";
|
||||
import { Loading } from "@multica/ui/components/ui/loading";
|
||||
import { ChatInput } from "@multica/ui/components/chat-input";
|
||||
import { MessageList } from "@multica/ui/components/message-list";
|
||||
import { useScrollFade } from "@multica/ui/hooks/use-scroll-fade";
|
||||
import { useAutoScroll } from "@multica/ui/hooks/use-auto-scroll";
|
||||
import { useGatewayConnection } from "@/hooks/use-gateway-connection";
|
||||
import { useChat } from "@/hooks/use-chat";
|
||||
import { ExecApprovalItem } from "@multica/ui/components/exec-approval-item";
|
||||
import { DevicePairing } from "@/components/device-pairing";
|
||||
import { ChatView } from "@multica/ui/components/chat-view";
|
||||
import { DevicePairing } from "@multica/ui/components/device-pairing";
|
||||
import { useGatewayConnection } from "@multica/hooks/use-gateway-connection";
|
||||
import { useChat } from "@multica/hooks/use-chat";
|
||||
|
||||
const ChatPage = () => {
|
||||
const { pageState, connectionState, identity, error, client, pairingKey, connect, disconnect } =
|
||||
|
|
@ -39,7 +33,7 @@ const ChatPage = () => {
|
|||
)}
|
||||
|
||||
{pageState === "connected" && client && identity && (
|
||||
<ChatView
|
||||
<ConnectedChat
|
||||
client={client}
|
||||
hubId={identity.hubId}
|
||||
agentId={identity.agentId}
|
||||
|
|
@ -51,7 +45,8 @@ const ChatPage = () => {
|
|||
);
|
||||
};
|
||||
|
||||
function ChatView({
|
||||
/** Thin wrapper that wires useChat hook to the shared ChatView */
|
||||
function ConnectedChat({
|
||||
client,
|
||||
hubId,
|
||||
agentId,
|
||||
|
|
@ -62,75 +57,9 @@ function ChatView({
|
|||
agentId: string;
|
||||
onDisconnect: () => void;
|
||||
}) {
|
||||
const { messages, streamingIds, isLoading, error, pendingApprovals, sendMessage, resolveApproval } = useChat({ client, hubId, agentId });
|
||||
const chat = useChat({ client, hubId, agentId });
|
||||
|
||||
const mainRef = useRef<HTMLElement>(null);
|
||||
const fadeStyle = useScrollFade(mainRef);
|
||||
useAutoScroll(mainRef);
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
<div className="container flex items-center justify-end p-2">
|
||||
<button
|
||||
onClick={onDisconnect}
|
||||
className="text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Disconnect
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<main ref={mainRef} className="flex-1 overflow-y-auto min-h-0" style={fadeStyle}>
|
||||
{messages.length === 0 && pendingApprovals.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-full text-muted-foreground text-sm">
|
||||
Your Agent is ready
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<MessageList messages={messages} streamingIds={streamingIds} />
|
||||
{pendingApprovals.length > 0 && (
|
||||
<div className="relative px-4 max-w-4xl mx-auto">
|
||||
{pendingApprovals.map((approval) => (
|
||||
<ExecApprovalItem
|
||||
key={approval.approvalId}
|
||||
command={approval.command}
|
||||
cwd={approval.cwd}
|
||||
riskLevel={approval.riskLevel}
|
||||
riskReasons={approval.riskReasons}
|
||||
expiresAtMs={approval.expiresAtMs}
|
||||
onDecision={(decision) => resolveApproval(approval.approvalId, decision)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</main>
|
||||
|
||||
{error && (
|
||||
<div className="container px-4" role="alert" aria-live="polite">
|
||||
<div className="rounded-lg bg-destructive/5 border border-destructive/15 text-xs px-3 py-2 flex items-center justify-between gap-3">
|
||||
<span className="text-foreground leading-snug">{error.message}</span>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={onDisconnect}
|
||||
className="shrink-0 text-xs h-7 px-2.5"
|
||||
>
|
||||
Disconnect
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<footer className="container p-2 pt-1">
|
||||
<ChatInput
|
||||
onSubmit={sendMessage}
|
||||
disabled={isLoading || !!error}
|
||||
placeholder={error ? "Connection error" : "Ask your Agent..."}
|
||||
/>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
return <ChatView {...chat} onDisconnect={onDisconnect} />;
|
||||
}
|
||||
|
||||
export default ChatPage;
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@
|
|||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@multica/hooks": "workspace:*",
|
||||
"@multica/sdk": "workspace:*",
|
||||
"@multica/store": "workspace:*",
|
||||
"@multica/ui": "workspace:*",
|
||||
|
|
|
|||
20
packages/hooks/package.json
Normal file
20
packages/hooks/package.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"name": "@multica/hooks",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.ts",
|
||||
"./*": "./src/*.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@multica/sdk": "workspace:*",
|
||||
"react": "catalog:",
|
||||
"uuid": "^13.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "catalog:",
|
||||
"@types/uuid": "^11.0.0",
|
||||
"typescript": "catalog:"
|
||||
}
|
||||
}
|
||||
15
packages/hooks/src/index.ts
Normal file
15
packages/hooks/src/index.ts
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
export { useGatewayConnection } from "./use-gateway-connection";
|
||||
export type {
|
||||
ConnectionIdentity,
|
||||
PageState,
|
||||
UseGatewayConnectionReturn,
|
||||
} from "./use-gateway-connection";
|
||||
|
||||
export { useChat } from "./use-chat";
|
||||
export type {
|
||||
Message,
|
||||
ToolStatus,
|
||||
ChatError,
|
||||
PendingApproval,
|
||||
UseChatReturn,
|
||||
} from "./use-chat";
|
||||
|
|
@ -49,6 +49,7 @@ export interface UseChatReturn {
|
|||
messages: Message[];
|
||||
streamingIds: Set<string>;
|
||||
isLoading: boolean;
|
||||
isLoadingHistory: boolean;
|
||||
error: ChatError | null;
|
||||
pendingApprovals: PendingApproval[];
|
||||
sendMessage: (text: string) => void;
|
||||
|
|
@ -75,6 +76,7 @@ export function useChat({ client, hubId, agentId }: UseChatOptions): UseChatRetu
|
|||
const [messages, setMessages] = useState<Message[]>([]);
|
||||
const [streamingIds, setStreamingIds] = useState<Set<string>>(new Set());
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isLoadingHistory, setIsLoadingHistory] = useState(true);
|
||||
const [error, setError] = useState<ChatError | null>(null);
|
||||
const [pendingApprovals, setPendingApprovals] = useState<PendingApproval[]>([]);
|
||||
// Keep a ref for use inside callbacks (avoids stale closures)
|
||||
|
|
@ -141,6 +143,8 @@ export function useChat({ client, hubId, agentId }: UseChatOptions): UseChatRetu
|
|||
}
|
||||
} catch {
|
||||
// History fetch is best-effort
|
||||
} finally {
|
||||
setIsLoadingHistory(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -321,5 +325,5 @@ export function useChat({ client, hubId, agentId }: UseChatOptions): UseChatRetu
|
|||
[client, hubId, agentId],
|
||||
);
|
||||
|
||||
return { messages, streamingIds, isLoading, error, pendingApprovals, sendMessage, resolveApproval };
|
||||
return { messages, streamingIds, isLoading, isLoadingHistory, error, pendingApprovals, sendMessage, resolveApproval };
|
||||
}
|
||||
20
packages/hooks/tsconfig.json
Normal file
20
packages/hooks/tsconfig.json
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "bundler",
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"esModuleInterop": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"jsx": "react-jsx"
|
||||
},
|
||||
"include": ["src/**/*"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
|
@ -117,7 +117,7 @@ export const ChatInput = forwardRef<ChatInputRef, ChatInputProps>(
|
|||
<EditorContent editor={editor} />
|
||||
<div className="flex items-center justify-end pt-2">
|
||||
<Button size="icon-lg" onClick={handleSubmit} disabled={disabled}>
|
||||
<HugeiconsIcon strokeWidth={2.5} icon={ArrowUpIcon} />
|
||||
<HugeiconsIcon className="size-4.5" strokeWidth={2.5} icon={ArrowUpIcon} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
168
packages/ui/src/components/chat-view.tsx
Normal file
168
packages/ui/src/components/chat-view.tsx
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
"use client";
|
||||
|
||||
import { useRef } from "react";
|
||||
import { Button } from "@multica/ui/components/ui/button";
|
||||
import { Skeleton } from "@multica/ui/components/ui/skeleton";
|
||||
import { ChatInput } from "@multica/ui/components/chat-input";
|
||||
import { MessageList } from "@multica/ui/components/message-list";
|
||||
import { MulticaIcon } from "@multica/ui/components/multica-icon";
|
||||
import { ExecApprovalItem } from "@multica/ui/components/exec-approval-item";
|
||||
import { useScrollFade } from "@multica/ui/hooks/use-scroll-fade";
|
||||
import { useAutoScroll } from "@multica/ui/hooks/use-auto-scroll";
|
||||
import type { Message } from "@multica/store";
|
||||
|
||||
export interface ChatViewError {
|
||||
code: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface ChatViewApproval {
|
||||
approvalId: string;
|
||||
command: string;
|
||||
cwd?: string;
|
||||
riskLevel: "safe" | "needs-review" | "dangerous";
|
||||
riskReasons: string[];
|
||||
expiresAtMs: number;
|
||||
}
|
||||
|
||||
export interface ChatViewProps {
|
||||
messages: Message[];
|
||||
streamingIds: Set<string>;
|
||||
isLoading: boolean;
|
||||
isLoadingHistory: boolean;
|
||||
error: ChatViewError | null;
|
||||
pendingApprovals: ChatViewApproval[];
|
||||
sendMessage: (text: string) => void;
|
||||
resolveApproval: (approvalId: string, decision: "allow-once" | "allow-always" | "deny") => void;
|
||||
onDisconnect: () => void;
|
||||
}
|
||||
|
||||
export function ChatView({
|
||||
messages,
|
||||
streamingIds,
|
||||
isLoading,
|
||||
isLoadingHistory,
|
||||
error,
|
||||
pendingApprovals,
|
||||
sendMessage,
|
||||
resolveApproval,
|
||||
onDisconnect,
|
||||
}: ChatViewProps) {
|
||||
const mainRef = useRef<HTMLElement>(null);
|
||||
const fadeStyle = useScrollFade(mainRef);
|
||||
useAutoScroll(mainRef);
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
<div className="container flex items-center justify-end px-4 py-2">
|
||||
<button
|
||||
onClick={onDisconnect}
|
||||
className="text-xs text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Disconnect
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<main ref={mainRef} className="flex-1 overflow-y-auto min-h-0" style={fadeStyle}>
|
||||
{isLoadingHistory && messages.length === 0 ? (
|
||||
<div className="px-4 py-6 max-w-4xl mx-auto">
|
||||
{/* User bubble */}
|
||||
<div className="flex justify-end my-2">
|
||||
<Skeleton className="h-8 w-[30%] rounded-md" />
|
||||
</div>
|
||||
{/* Assistant multi-line */}
|
||||
<div className="space-y-2 py-1 px-2.5 my-1">
|
||||
<Skeleton className="h-3.5 w-full" />
|
||||
<Skeleton className="h-3.5 w-[88%]" />
|
||||
<Skeleton className="h-3.5 w-[65%]" />
|
||||
</div>
|
||||
{/* Tool row */}
|
||||
<div className="px-2.5 my-1">
|
||||
<Skeleton className="h-6 w-44 rounded" />
|
||||
</div>
|
||||
{/* Assistant short reply */}
|
||||
<div className="space-y-2 py-1 px-2.5 my-1">
|
||||
<Skeleton className="h-3.5 w-[92%]" />
|
||||
<Skeleton className="h-3.5 w-[55%]" />
|
||||
</div>
|
||||
{/* User bubble */}
|
||||
<div className="flex justify-end my-2">
|
||||
<Skeleton className="h-8 w-[42%] rounded-md" />
|
||||
</div>
|
||||
{/* Assistant reply */}
|
||||
<div className="space-y-2 py-1 px-2.5 my-1">
|
||||
<Skeleton className="h-3.5 w-full" />
|
||||
<Skeleton className="h-3.5 w-[80%]" />
|
||||
<Skeleton className="h-3.5 w-[70%]" />
|
||||
<Skeleton className="h-3.5 w-[40%]" />
|
||||
</div>
|
||||
{/* User bubble */}
|
||||
<div className="flex justify-end my-2">
|
||||
<Skeleton className="h-8 w-[22%] rounded-md" />
|
||||
</div>
|
||||
{/* Assistant reply */}
|
||||
<div className="space-y-2 py-1 px-2.5 my-1">
|
||||
<Skeleton className="h-3.5 w-[75%]" />
|
||||
<Skeleton className="h-3.5 w-[50%]" />
|
||||
</div>
|
||||
</div>
|
||||
) : messages.length === 0 && pendingApprovals.length === 0 ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="flex items-center gap-3 text-muted-foreground">
|
||||
<MulticaIcon className="size-5 shrink-0" />
|
||||
<div>
|
||||
<p className="text-sm font-medium">Start a conversation</p>
|
||||
<p className="text-xs text-muted-foreground/70">
|
||||
Type a message below to chat with your Agent
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<MessageList messages={messages} streamingIds={streamingIds} />
|
||||
{pendingApprovals.length > 0 && (
|
||||
<div className="relative px-4 max-w-4xl mx-auto">
|
||||
{pendingApprovals.map((approval) => (
|
||||
<ExecApprovalItem
|
||||
key={approval.approvalId}
|
||||
command={approval.command}
|
||||
cwd={approval.cwd}
|
||||
riskLevel={approval.riskLevel}
|
||||
riskReasons={approval.riskReasons}
|
||||
expiresAtMs={approval.expiresAtMs}
|
||||
onDecision={(decision) => resolveApproval(approval.approvalId, decision)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</main>
|
||||
|
||||
{error && (
|
||||
<div className="container px-4" role="alert" aria-live="polite">
|
||||
<div className="rounded-lg bg-destructive/5 border border-destructive/15 text-xs px-3 py-2 flex items-center justify-between gap-3">
|
||||
<span className="text-foreground leading-snug">{error.message}</span>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={onDisconnect}
|
||||
className="shrink-0 text-xs h-7 px-2.5"
|
||||
>
|
||||
Disconnect
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<footer className="container px-4 pb-2 pt-1">
|
||||
<ChatInput
|
||||
onSubmit={sendMessage}
|
||||
disabled={isLoading || !!error}
|
||||
placeholder={error ? "Connection error" : "Ask your Agent..."}
|
||||
/>
|
||||
</footer>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -13,8 +13,14 @@ import {
|
|||
Alert02Icon,
|
||||
} from "@hugeicons/core-free-icons";
|
||||
import { QrScannerView } from "@multica/ui/components/qr-scanner-view";
|
||||
import { MulticaIcon } from "@multica/ui/components/multica-icon";
|
||||
import { parseConnectionCode } from "@multica/store";
|
||||
import type { ConnectionIdentity } from "@/hooks/use-gateway-connection";
|
||||
|
||||
export interface ConnectionIdentity {
|
||||
gateway: string;
|
||||
hubId: string;
|
||||
agentId: string;
|
||||
}
|
||||
|
||||
export interface DevicePairingProps {
|
||||
connectionState: string;
|
||||
|
|
@ -202,7 +208,10 @@ export function DevicePairing({
|
|||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full gap-4 px-4">
|
||||
<div className="text-center space-y-1">
|
||||
<p className="text-base font-medium">Scan to connect</p>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<MulticaIcon className="size-4.5 text-muted-foreground/50" />
|
||||
<p className="text-base font-medium">Scan to connect</p>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Scan a Multica QR code to connect to your agent
|
||||
</p>
|
||||
|
|
@ -216,9 +225,12 @@ export function DevicePairing({
|
|||
return (
|
||||
<div className="flex flex-col items-center justify-center h-full gap-4 px-4 mb-28">
|
||||
<div className="text-center space-y-1">
|
||||
<p className="text-base font-medium">
|
||||
{mode === "scan" ? "Scan to connect" : "Paste to connect"}
|
||||
</p>
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<MulticaIcon className="size-4.5 text-muted-foreground/50" />
|
||||
<p className="text-base font-medium">
|
||||
{mode === "scan" ? "Scan to connect" : "Paste to connect"}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{mode === "scan"
|
||||
? "Scan a Multica QR code to connect to your agent"
|
||||
33
packages/ui/src/components/multica-icon.tsx
Normal file
33
packages/ui/src/components/multica-icon.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { cn } from "@multica/ui/lib/utils";
|
||||
|
||||
/**
|
||||
* Pure CSS 8-pointed asterisk icon matching the Multica logo.
|
||||
* Uses currentColor so it adapts to light/dark themes automatically.
|
||||
* Clip-path polygon traced from the original SVG path coordinates.
|
||||
*/
|
||||
export function MulticaIcon({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"span">) {
|
||||
return (
|
||||
<span
|
||||
className={cn("inline-block size-[1em]", className)}
|
||||
aria-hidden="true"
|
||||
{...props}
|
||||
>
|
||||
<span
|
||||
className="block size-full bg-current"
|
||||
style={{
|
||||
clipPath: `polygon(
|
||||
45% 62.1%, 45% 100%, 55% 100%, 55% 62.1%,
|
||||
81.8% 88.9%, 88.9% 81.8%, 62.1% 55%, 100% 55%,
|
||||
100% 45%, 62.1% 45%, 88.9% 18.2%, 81.8% 11.1%,
|
||||
55% 37.9%, 55% 0%, 45% 0%, 45% 37.9%,
|
||||
18.2% 11.1%, 11.1% 18.2%, 37.9% 45%, 0% 45%,
|
||||
0% 55%, 37.9% 55%, 11.1% 81.8%, 18.2% 88.9%
|
||||
)`,
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
29
pnpm-lock.yaml
generated
29
pnpm-lock.yaml
generated
|
|
@ -371,6 +371,9 @@ importers:
|
|||
'@hugeicons/react':
|
||||
specifier: ^1.1.4
|
||||
version: 1.1.4(react@19.2.3)
|
||||
'@multica/hooks':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/hooks
|
||||
'@multica/sdk':
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/sdk
|
||||
|
|
@ -418,6 +421,28 @@ importers:
|
|||
specifier: 'catalog:'
|
||||
version: 5.9.3
|
||||
|
||||
packages/hooks:
|
||||
dependencies:
|
||||
'@multica/sdk':
|
||||
specifier: workspace:*
|
||||
version: link:../sdk
|
||||
react:
|
||||
specifier: 'catalog:'
|
||||
version: 19.2.3
|
||||
uuid:
|
||||
specifier: ^13.0.0
|
||||
version: 13.0.0
|
||||
devDependencies:
|
||||
'@types/react':
|
||||
specifier: 'catalog:'
|
||||
version: 19.1.17
|
||||
'@types/uuid':
|
||||
specifier: ^11.0.0
|
||||
version: 11.0.0
|
||||
typescript:
|
||||
specifier: 'catalog:'
|
||||
version: 5.9.3
|
||||
|
||||
packages/sdk:
|
||||
dependencies:
|
||||
socket.io-client:
|
||||
|
|
@ -14919,7 +14944,7 @@ snapshots:
|
|||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1)):
|
||||
eslint-module-utils@2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1)):
|
||||
dependencies:
|
||||
debug: 3.2.7
|
||||
optionalDependencies:
|
||||
|
|
@ -14950,7 +14975,7 @@ snapshots:
|
|||
doctrine: 2.1.0
|
||||
eslint: 9.39.2(jiti@2.6.1)
|
||||
eslint-import-resolver-node: 0.3.9
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1(eslint-plugin-import@2.32.0)(eslint@9.39.2(jiti@2.6.1)))(eslint@9.39.2(jiti@2.6.1))
|
||||
eslint-module-utils: 2.12.1(@typescript-eslint/parser@8.54.0(eslint@9.39.2(jiti@2.6.1))(typescript@5.9.3))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.10.1)(eslint@9.39.2(jiti@2.6.1))
|
||||
hasown: 2.0.2
|
||||
is-core-module: 2.16.1
|
||||
is-glob: 4.0.3
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue