- Add intelligent-router.sh hook for automatic agent routing - Add AUTO-TRIGGER-SUMMARY.md documentation - Add FINAL-INTEGRATION-SUMMARY.md documentation - Complete Prometheus integration (6 commands + 4 tools) - Complete Dexto integration (12 commands + 5 tools) - Enhanced Ralph with access to all agents - Fix /clawd command (removed disable-model-invocation) - Update hooks.json to v5 with intelligent routing - 291 total skills now available - All 21 commands with automatic routing 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
204 lines
6.5 KiB
TypeScript
204 lines
6.5 KiB
TypeScript
import { useMemo } from 'react';
|
|
import { useQueries } from '@tanstack/react-query';
|
|
import { client } from '@/lib/client';
|
|
|
|
type NormalizedResourceItem =
|
|
| {
|
|
kind: 'text';
|
|
text: string;
|
|
mimeType?: string;
|
|
}
|
|
| {
|
|
kind: 'image';
|
|
src: string;
|
|
mimeType: string;
|
|
alt?: string;
|
|
}
|
|
| {
|
|
kind: 'audio';
|
|
src: string;
|
|
mimeType: string;
|
|
filename?: string;
|
|
}
|
|
| {
|
|
kind: 'video';
|
|
src: string;
|
|
mimeType: string;
|
|
filename?: string;
|
|
}
|
|
| {
|
|
kind: 'file';
|
|
src?: string;
|
|
mimeType?: string;
|
|
filename?: string;
|
|
};
|
|
|
|
export interface NormalizedResource {
|
|
uri: string;
|
|
name?: string;
|
|
meta?: Record<string, unknown>;
|
|
items: NormalizedResourceItem[];
|
|
}
|
|
|
|
export interface ResourceState {
|
|
status: 'loading' | 'loaded' | 'error';
|
|
data?: NormalizedResource;
|
|
error?: string;
|
|
}
|
|
|
|
type ResourceStateMap = Record<string, ResourceState>;
|
|
|
|
function buildDataUrl(base64: string, mimeType: string): string {
|
|
return `data:${mimeType};base64,${base64}`;
|
|
}
|
|
|
|
function normalizeResource(uri: string, payload: any): NormalizedResource {
|
|
const contents = Array.isArray(payload?.contents) ? payload.contents : [];
|
|
const meta = (payload?._meta ?? {}) as Record<string, unknown>;
|
|
const name =
|
|
(typeof meta.originalName === 'string' && meta.originalName.trim().length > 0
|
|
? meta.originalName
|
|
: undefined) || uri;
|
|
|
|
const items: NormalizedResourceItem[] = [];
|
|
|
|
for (const item of contents) {
|
|
if (!item || typeof item !== 'object') continue;
|
|
|
|
if (typeof (item as { text?: unknown }).text === 'string') {
|
|
items.push({
|
|
kind: 'text',
|
|
text: (item as { text: string }).text,
|
|
mimeType: typeof item.mimeType === 'string' ? item.mimeType : undefined,
|
|
});
|
|
continue;
|
|
}
|
|
|
|
const blobData = typeof item.blob === 'string' ? item.blob : undefined;
|
|
const rawData = typeof item.data === 'string' ? item.data : undefined;
|
|
const mimeType = typeof item.mimeType === 'string' ? item.mimeType : undefined;
|
|
const filename = typeof item.filename === 'string' ? item.filename : undefined;
|
|
|
|
if ((blobData || rawData) && mimeType) {
|
|
const base64 = blobData ?? rawData!;
|
|
const src = buildDataUrl(base64, mimeType);
|
|
if (mimeType.startsWith('image/')) {
|
|
items.push({
|
|
kind: 'image',
|
|
src,
|
|
mimeType,
|
|
alt: filename || name,
|
|
});
|
|
} else if (mimeType.startsWith('audio/')) {
|
|
items.push({
|
|
kind: 'audio',
|
|
src,
|
|
mimeType,
|
|
filename: filename || name,
|
|
});
|
|
} else if (mimeType.startsWith('video/')) {
|
|
items.push({
|
|
kind: 'video',
|
|
src,
|
|
mimeType,
|
|
filename: filename || name,
|
|
});
|
|
} else {
|
|
items.push({
|
|
kind: 'file',
|
|
src,
|
|
mimeType,
|
|
filename: filename || name,
|
|
});
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if (mimeType && mimeType.startsWith('text/') && typeof item.value === 'string') {
|
|
items.push({
|
|
kind: 'text',
|
|
text: item.value,
|
|
mimeType,
|
|
});
|
|
}
|
|
}
|
|
|
|
return {
|
|
uri,
|
|
name,
|
|
meta,
|
|
items,
|
|
};
|
|
}
|
|
|
|
async function fetchResourceContent(uri: string): Promise<NormalizedResource> {
|
|
const response = await client.api.resources[':resourceId'].content.$get({
|
|
param: { resourceId: encodeURIComponent(uri) },
|
|
});
|
|
const body = await response.json();
|
|
const contentPayload = body?.content;
|
|
if (!contentPayload) {
|
|
throw new Error('No content returned for resource');
|
|
}
|
|
return normalizeResource(uri, contentPayload);
|
|
}
|
|
|
|
export function useResourceContent(resourceUris: string[]): ResourceStateMap {
|
|
// Serialize array for stable dependency comparison.
|
|
// Arrays are compared by reference in React, so ['a','b'] !== ['a','b'] even though
|
|
// values are identical. Serializing to 'a|b' allows value-based comparison to avoid
|
|
// unnecessary re-computation when parent passes new array reference with same contents.
|
|
const serializedUris = resourceUris.join('|');
|
|
|
|
const normalizedUris = useMemo(() => {
|
|
const seen = new Set<string>();
|
|
const ordered: string[] = [];
|
|
for (const uri of resourceUris) {
|
|
if (!uri || typeof uri !== 'string') continue;
|
|
const trimmed = uri.trim();
|
|
if (!trimmed || seen.has(trimmed)) continue;
|
|
seen.add(trimmed);
|
|
ordered.push(trimmed);
|
|
}
|
|
return ordered;
|
|
// We use resourceUris inside but only depend on serializedUris. This is safe because
|
|
// serializedUris is derived from resourceUris - when the string changes, the array
|
|
// values changed too. This is an intentional optimization to prevent re-runs when
|
|
// array reference changes but values remain the same.
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [serializedUris]);
|
|
|
|
const queries = useQueries({
|
|
queries: normalizedUris.map((uri) => ({
|
|
queryKey: ['resourceContent', uri],
|
|
queryFn: () => fetchResourceContent(uri),
|
|
enabled: !!uri,
|
|
retry: false,
|
|
})),
|
|
});
|
|
|
|
const resources: ResourceStateMap = useMemo(() => {
|
|
const result: ResourceStateMap = {};
|
|
queries.forEach((query, index) => {
|
|
const uri = normalizedUris[index];
|
|
if (!uri) return;
|
|
|
|
if (query.isLoading) {
|
|
result[uri] = { status: 'loading' };
|
|
} else if (query.error) {
|
|
result[uri] = {
|
|
status: 'error',
|
|
error: query.error instanceof Error ? query.error.message : String(query.error),
|
|
};
|
|
} else if (query.data) {
|
|
result[uri] = { status: 'loaded', data: query.data };
|
|
}
|
|
});
|
|
return result;
|
|
}, [queries, normalizedUris]);
|
|
|
|
return resources;
|
|
}
|
|
|
|
export type { NormalizedResourceItem };
|