feat(agent): support Responses steering and project-aware tools

Use the dedicated TJWATER_AGENT_URL and Responses stream protocol. Keep running
turns active while users steer or queue follow-ups and edit pending delivery.
Refresh the model catalog before generation and preserve approval mode selection.

Integrate scoped investigation messaging/presentation, geographic place selection,
current Agent composer styles and supporting workbench contracts.

Validation: 77 focused Vitest checks, 21 browser cases, lint, typecheck and
production build. Preserve direct Server/Agent routing and project authentication.
This commit is contained in:
2026-10-19 18:46:58 +08:00
parent 2c5bf15cce
commit ec23d44b02
131 changed files with 6663 additions and 1204 deletions
@@ -0,0 +1,132 @@
import { useCallback, useId, useRef, useState, type ReactNode, type Ref } from "react";
import { createPortal } from "react-dom";
import * as Dialog from "@radix-ui/react-dialog";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { Maximize2, X } from "lucide-react";
import { Button } from "@/shared/ui/button";
import { cn } from "@/shared/ui/cn";
/** Report-specific progressive disclosure. The source stays mounted while reading. */
export function InvestigationExpandCard({
title, label = title, preview, children, wide = false, onOpen, contentRef,
triggerLabel = `展开${title}`, className, revision, disabled = false, reuseContent = false
}: {
title: string;
label?: string;
preview: ReactNode;
children: ReactNode | ((close: (restoreFocus?: boolean) => void) => ReactNode);
wide?: boolean;
onOpen?: () => void;
contentRef?: Ref<HTMLElement>;
triggerLabel?: string;
className?: string;
/** Freeze versioned prose while reading; live controls must not use this. */
revision?: string;
disabled?: boolean;
/** Move one live content host between slots, preserving expensive renderers. */
reuseContent?: boolean;
}) {
const id = useId();
const triggerRef = useRef<HTMLButtonElement>(null);
const restoreFocusRef = useRef(true);
const [open, setOpen] = useState(false);
const [keyboard, setKeyboard] = useState(false);
const [reading, setReading] = useState<{ revision: string; content: typeof children } | null>(null);
const [contentHost] = useState(() => reuseContent ? document.createElement("div") : null);
const previewSlot = useRef<HTMLDivElement | null>(null);
const attachPreview = useCallback((slot: HTMLDivElement | null) => {
previewSlot.current = slot;
if (slot && contentHost && !contentHost.isConnected) slot.appendChild(contentHost);
}, [contentHost]);
const attachDetail = useCallback((slot: HTMLDivElement | null) => {
const preview = previewSlot.current;
if (!preview || !contentHost) return;
if (slot) {
// Reserve only the vacated inline space while expanded. The graph itself
// keeps its natural height, including after content updates or resizing.
// Use the layout height, not a bounding box scaled by the card animation.
preview.style.height = getComputedStyle(preview).height;
slot.appendChild(contentHost);
} else {
preview.appendChild(contentHost);
preview.style.removeProperty("height");
}
}, [contentHost]);
const reduced = useReducedMotion();
const duration = reduced || keyboard ? 0 : 0.24;
const transition = { duration, ease: [0.22, 1, 0.36, 1] as const };
const close = (restoreFocus = true) => { restoreFocusRef.current = restoreFocus; setOpen(false); };
const content = reading?.content ?? children;
return (
<Dialog.Root open={open} onOpenChange={(next) => {
setOpen(next);
if (next) {
restoreFocusRef.current = true;
setReading(revision === undefined ? null : { revision, content: children });
onOpen?.();
}
}}>
<motion.section
ref={contentRef}
aria-label={label}
className={cn("investigation-report-section investigation-expand-card", className)}
layoutId={reduced || keyboard ? undefined : id}
layoutDependency={open}
transition={transition}
style={{ borderRadius: 12 }}
>
<div className="investigation-card-heading">
<h3 className="text-sm font-semibold text-[var(--ink)]">{title}</h3>
<Dialog.Trigger asChild>
<Button ref={triggerRef} variant="ghost" size="icon-sm" aria-label={triggerLabel} disabled={disabled}
title={triggerLabel} onClick={(event) => setKeyboard(event.detail === 0)}>
<Maximize2 size={15} aria-hidden />
</Button>
</Dialog.Trigger>
</div>
{/* Contain child margins so the occupied and vacated slots measure alike. */}
{contentHost ? <div ref={attachPreview} className="flow-root" /> : preview}
</motion.section>
{contentHost ? createPortal(typeof children === "function" ? children(close) : children, contentHost) : null}
<Dialog.Portal forceMount>
<AnimatePresence initial={false}>
{open ? (
<Dialog.Overlay forceMount asChild key="backdrop">
<motion.div className="investigation-detail-backdrop"
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }} transition={transition} />
</Dialog.Overlay>
) : null}
{open ? (
<div className="investigation-detail-positioner" key="detail">
<Dialog.Content forceMount asChild aria-describedby={undefined}
onEscapeKeyDown={(event) => { event.stopPropagation(); setKeyboard(true); }}
onCloseAutoFocus={(event) => { event.preventDefault(); if (restoreFocusRef.current) triggerRef.current?.focus({ preventScroll: true }); }}>
<motion.section className={cn("investigation-detail-panel", wide && "investigation-detail-panel-wide")}
layoutId={reduced || keyboard ? undefined : id} layoutDependency={open}
initial={{ opacity: 0 }} animate={{ opacity: 1 }} exit={{ opacity: 0 }}
transition={transition} style={{ borderRadius: 12 }}>
<header className="investigation-detail-heading">
<Dialog.Title className="text-base font-semibold text-[var(--ink)]">{title}</Dialog.Title>
<Dialog.Close asChild>
<Button variant="ghost" size="icon-sm" aria-label="关闭详情"><X size={17} aria-hidden /></Button>
</Dialog.Close>
</header>
<div className="investigation-detail-scroll">
{contentHost ? <div ref={attachDetail} /> : typeof content === "function" ? content(close) : content}
</div>
{reading && revision !== undefined && reading.revision !== revision ? (
<div className="investigation-report-update" role="status">
<span>已有更新,当前保留正在阅读的版本。</span>
<Button variant="outline" size="sm" onClick={() => setReading({ revision, content: children })}>查看最新内容</Button>
</div>
) : null}
</motion.section>
</Dialog.Content>
</div>
) : null}
</AnimatePresence>
</Dialog.Portal>
</Dialog.Root>
);
}