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,166 @@
import { ChevronDown, LocateFixed, MapPin, Search, Trash2 } from "lucide-react";
import { useId, useLayoutEffect, useRef } from "react";
import { Button } from "@/shared/ui/button";
import { Input } from "@/shared/ui/input";
import { cn } from "@/shared/ui/cn";
import type { WorkbenchGeocoding } from "../hooks/use-workbench-geocoding";
import { MAX_PLACE_SEARCH_RESULTS, type PlaceSearchItem } from "../api/place-search-client";
const RESULT_VIEWPORT_CLASS_NAME = "h-72 min-h-0 max-h-72 overflow-y-auto overscroll-contain pr-1 [scrollbar-gutter:stable] [scrollbar-width:thin]";
function PlaceSummary({ item, index, annotation = item.district }: { item: PlaceSearchItem; index?: number; annotation?: string }) {
return (
<>
{index !== undefined ? <span aria-hidden="true" className="w-5 shrink-0 self-start text-center text-[11px] font-normal leading-5 tabular-nums text-[var(--ink-muted)]">
{String(index + 1).padStart(2, "0")}
</span> : null}
<span className="min-w-0 flex-1 space-y-1">
<span className="flex items-start justify-between gap-2 leading-5">
<span className="min-w-0 flex-1 break-words font-semibold text-[var(--ink)]">{item.name}</span>
<span aria-label={index === undefined && item.category ? `地点类型:${item.category}` : undefined} className="max-w-[40%] shrink-0 break-words text-right text-[11px] font-normal text-[var(--ink-muted)]">{annotation}</span>
</span>
<span className="block break-words text-[11px] font-normal leading-4 text-[var(--ink-muted)]">{item.address}</span>
</span>
</>
);
}
export function GeocodingPanel({ geocoding }: { geocoding: WorkbenchGeocoding }) {
const inputId = useId();
const statusId = useId();
const candidateListRef = useRef<HTMLUListElement | null>(null);
const changeSelectionRef = useRef<HTMLButtonElement | null>(null);
const { address, result, results, pending, error, enabled } = geocoding;
const selectionCollapsed = Boolean(result && !geocoding.choosing);
const hideSelectionHint = Boolean(results?.items.length && enabled && !error);
useLayoutEffect(() => {
if (selectionCollapsed || !result) return;
const list = candidateListRef.current;
const selected = list?.querySelector<HTMLButtonElement>('button[aria-pressed="true"]');
if (!list || !selected) return;
const viewport = list.getBoundingClientRect();
const row = selected.getBoundingClientRect();
list.scrollTop += row.top - viewport.top - (list.clientHeight - row.height) / 2;
}, [selectionCollapsed, result, results]);
const status = error
?? (pending ? "正在搜索地名…"
: !enabled ? "等待地图加载后即可定位"
: result ? "已在地图标出所选位置"
: results ? results.items.length ? "选择一个候选位置进行定位" : "奉贤区内未找到匹配位置,请细化关键字"
: "搜索奉贤区内地址或地名,支持回车查询");
return (
<div className="flex min-h-0 flex-1 flex-col gap-3">
<form
className="flex shrink-0 flex-col gap-2"
onSubmit={(event) => {
event.preventDefault();
void geocoding.search();
}}
>
<label htmlFor={inputId} className="block px-1 text-xs font-semibold text-[var(--ink-muted)]">
查询地址
</label>
<div className="flex items-start gap-2">
<Input
id={inputId}
value={address}
onChange={(event) => geocoding.changeAddress(event.target.value)}
placeholder="道路、门牌号或地名"
className="h-10 min-w-0 flex-1 rounded-xl border-[var(--surface-border)] bg-[var(--surface-reading)] text-xs shadow-none md:text-xs"
autoComplete="street-address"
maxLength={120}
aria-invalid={Boolean(error)}
aria-describedby={statusId}
/>
<Button
type="submit"
variant="outline"
aria-label="搜索地名"
className="shrink-0 gap-1.5 px-3 text-xs"
disabled={!enabled || !address.trim()}
loading={pending}
loadingLabel="正在搜索地名"
>
<Search aria-hidden="true" />查询
</Button>
</div>
<div
id={statusId}
role={error ? "alert" : "status"}
aria-live={error ? "assertive" : "polite"}
className={hideSelectionHint ? "sr-only" : cn("flex min-h-5 items-start gap-1.5 px-1 text-[11px] leading-5", error ? "text-[var(--status-danger-foreground)]" : "text-[var(--ink-muted)]")}
>
{result && !error ? <MapPin size={13} className="mt-0.5 shrink-0" aria-hidden="true" /> : null}
<span>{status}</span>
</div>
</form>
{results && results.items.length > 0 ? (
<section aria-label="搜索结果" className="flex min-h-0 flex-1 flex-col gap-2 border-t border-[var(--surface-divider)] pt-3">
<div className="flex h-10 shrink-0 items-center justify-between gap-2 px-1 text-[11px] text-[var(--ink-muted)]">
<span className="text-xs font-semibold">{selectionCollapsed ? "已选位置" : "选择位置"}</span>
{selectionCollapsed ? (
<Button ref={changeSelectionRef} type="button" variant="secondary" className="gap-1.5 px-3 text-xs" aria-expanded={false}
onClick={(event) => {
geocoding.changeSelection();
if (event.detail === 0) window.requestAnimationFrame(() => {
const list = candidateListRef.current;
const selected = list?.querySelector<HTMLButtonElement>('button[aria-pressed="true"]');
(selected ?? list?.querySelector<HTMLButtonElement>("button"))?.focus({ preventScroll: true });
});
}}>
更换位置<ChevronDown aria-hidden="true" />
</Button>
) : <span className="tabular-nums">{results.truncated ? `前 ${results.total} 个结果` : `共 ${results.total} 个结果`}</span>}
</div>
{!selectionCollapsed ? <ul ref={candidateListRef} aria-busy={pending} className={cn(RESULT_VIEWPORT_CLASS_NAME, "bg-transparent", pending && "opacity-60")}>
{results.items.map((item, index) => (
<li key={item.id} className="border-b border-[var(--surface-divider)] last:border-b-0">
<Button type="button" variant="ghost" aria-pressed={result?.id === item.id} data-selection-tone="soft"
aria-label={`定位 ${item.name} ${item.address}`} disabled={pending || !enabled}
className={cn("h-auto min-h-16 w-full justify-start gap-3 rounded-lg border-0 whitespace-normal px-1 py-3 text-left text-xs focus-visible:ring-inset focus-visible:ring-offset-0 [@media(hover:hover)]:hover:bg-[var(--surface-control)] [@media(hover:hover)]:hover:text-[var(--ink)]",
result?.id === item.id && "bg-[var(--action-selection-soft)] text-[var(--action-blue-hover)] [&_span]:text-[var(--action-blue-hover)] [@media(hover:hover)]:hover:bg-[var(--action-selection-soft)]")}
onClick={(event) => {
geocoding.select(item.id);
if (event.detail === 0) window.requestAnimationFrame(() => changeSelectionRef.current?.focus());
}}>
<PlaceSummary item={item} index={index} />
</Button>
</li>
))}
</ul> : null}
{result && selectionCollapsed ? (
<section aria-label="定位结果" className={cn(RESULT_VIEWPORT_CLASS_NAME, "flex flex-col gap-2 overflow-hidden")}>
<div className="min-h-0 flex-1 overflow-y-auto overscroll-contain [scrollbar-width:thin]">
<div className="space-y-3">
<div className="flex min-h-16 items-center gap-3 border-b border-[var(--surface-divider)] px-1 py-3 text-xs">
<PlaceSummary item={result} annotation={result.category ?? result.district} />
</div>
<dl aria-label="经纬度坐标" className="space-y-1 px-1">
<dt className="text-[11px] leading-4 text-[var(--ink-muted)]">经度 / 纬度</dt>
<dd className="whitespace-nowrap text-sm font-medium leading-5 tabular-nums text-[var(--ink)]">
{`${result.longitude.toFixed(6)}, ${result.latitude.toFixed(6)}`}
</dd>
</dl>
</div>
</div>
<div className="grid shrink-0 grid-cols-2 gap-2">
<Button type="button" variant="secondary" className="text-xs" disabled={!enabled} onClick={geocoding.locate}>
<LocateFixed aria-hidden="true" />重新定位
</Button>
<Button type="button" variant="secondary" className="text-xs" onClick={geocoding.clear}>
<Trash2 aria-hidden="true" />清除结果
</Button>
</div>
</section>
) : null}
<p className="flex min-h-5 shrink-0 items-start gap-1.5 px-1 text-[11px] leading-5 text-[var(--ink-muted)]">
{selectionCollapsed ? <><MapPin size={13} className="mt-0.5 shrink-0" aria-hidden="true" /><span>已在地图标出所选位置</span></>
: results.truncated ? `最多显示 ${MAX_PLACE_SEARCH_RESULTS} 个位置,请细化关键词`
: "选择一个候选位置进行定位"}
</p>
</section>
) : null}
</div>
);
}