import { AssistantRuntimeProvider } from "@assistant-ui/react"; import { useEffect, useState, useSyncExternalStore } from "react"; import type { ImageAttachment, ModelConnectionInput } from "@agent-studio/shared"; import { HarnessAttachmentAdapter } from "./assistant/attachment-adapter.js"; import { useHarnessRuntime, type HarnessRuntimeStore } from "./assistant/use-harness-runtime.js"; import { Thread } from "./components/chat/Thread.js"; import { AppShell } from "./components/layout/AppShell.js"; import { SkillsPanel } from "./components/skills/SkillsPanel.js"; import { EventTimeline } from "./components/timeline/EventTimeline.js"; import { ApprovalDialog } from "./components/approvals/ApprovalDialog.js"; import { ModelSettings, type ModelView } from "./components/settings/ModelSettings.js"; import { CompletionAnnouncer } from "./components/common/CompletionAnnouncer.js"; import { HarnessClientStore } from "./store/harness-client-store.js"; type AppStore = HarnessRuntimeStore & { start(): Promise; getModelConfig(): Promise<{ modelConfig: ModelView }>; putModelConfig?: (input: ModelConnectionInput) => Promise<{ modelConfig: ModelView }>; testModelConfig?: () => Promise<{ status: "ok" | "deferred"; message: string }>; clearCredentials?: () => Promise; approve?: (approvalId: string) => Promise; deny?: (approvalId: string) => Promise; uploadAttachment?: (file: File) => Promise<{ attachment: ImageAttachment }>; deleteAttachment?: (attachmentId: string) => Promise; }; export type AppProps = { store?: AppStore; autoStart?: boolean; modelConfigured?: boolean; }; export function App({ store: suppliedStore, autoStart = true, modelConfigured: suppliedModelConfigured }: AppProps) { const [ownedStore] = useState(() => new HarnessClientStore()); const store = suppliedStore ?? ownedStore; const [settingsOpen, setSettingsOpen] = useState(false); const snapshot = useSyncExternalStore( (subscriber) => store.subscribe(subscriber), () => store.getSnapshot(), () => store.getSnapshot(), ); const [model, setModel] = useState({ configured: suppliedModelConfigured ?? false, source: suppliedModelConfigured === true ? "session" : "missing", modelId: suppliedModelConfigured === true ? "Configured model" : "No model", supportsImages: false, }); const [attachmentAdapter] = useState(() => { return store.uploadAttachment === undefined || store.deleteAttachment === undefined ? undefined : new HarnessAttachmentAdapter({ uploadAttachment: (file) => store.uploadAttachment?.(file) ?? Promise.reject(new Error("附件上传接口不可用")), deleteAttachment: (attachmentId) => store.deleteAttachment?.(attachmentId) ?? Promise.reject(new Error("附件删除接口不可用")), }); }); const activeAttachmentAdapter = model.supportsImages === true ? attachmentAdapter : undefined; const runtime = useHarnessRuntime(store, { isSendDisabled: !model.configured, ...(activeAttachmentAdapter === undefined ? {} : { attachmentAdapter: activeAttachmentAdapter }), }); const busy = Object.values(snapshot.runs).some((run) => run.status === "running"); const approvalPending = Object.values(snapshot.approvals).some((approval) => approval.status === "pending"); const pendingApproval = Object.values(snapshot.approvals).find((approval) => approval.status === "pending"); const latestAssistantStatus = [...snapshot.messageOrder] .reverse() .map((messageId) => snapshot.messages[messageId]) .find((message) => message?.role === "assistant")?.status; useEffect(() => { if (!autoStart) return undefined; let active = true; void store.start().then(async () => { const response = await store.getModelConfig(); if (active) setModel(response.modelConfig); }).catch(() => undefined); return () => { active = false; }; }, [autoStart, store]); useEffect(() => () => attachmentAdapter?.dispose(), [attachmentAdapter]); return ( setSettingsOpen(true), onReset: () => window.location.reload(), }} skills={} chat={} timeline={} /> {pendingApproval !== undefined ? ( store.approve?.(approvalId) ?? Promise.reject(new Error("Approval API unavailable"))} onDeny={(approvalId) => store.deny?.(approvalId) ?? Promise.reject(new Error("Approval API unavailable"))} /> ) : null} setSettingsOpen(false)} onSave={async (input) => { if (store.putModelConfig === undefined) throw new Error("模型配置接口不可用"); const result = await store.putModelConfig(input); setModel(result.modelConfig); return result.modelConfig; }} onTest={async () => { if (store.testModelConfig === undefined) throw new Error("连接测试接口不可用"); return store.testModelConfig(); }} onClear={async () => { if (store.clearCredentials !== undefined) await store.clearCredentials(); setModel((current) => { const next = { ...current, configured: false, source: "missing" as const }; delete next.keyHint; return next; }); }} /> ); }