feat(agent): 抽屉与会话页统一成一个对话界面,子会话在父会话里实时可见
- 抽屉和会话页渲染同一套 GoalComposer / AgentRunPanel;抽屉标题即最近会话切换, 头部只有新对话、在页面打开、关闭;去掉右下角悬浮按钮和最小化小窗 - 会话列表合一:对话与巡检混排带类型小标签,搜索 + 一个状态下拉,只列顶层会话 (GET /agent/runs?roots=true,新增可选参数) - 子会话卡片:由镜像的 subagent_* 事件折成,显示任务、状态、当前步骤、用时、调用次数、费用, 可展开看过程、可打开;顶部「N 个子会话运行中」跳到卡片;subagent_completed 加可选 durationMs - 回答去内部字段:汇报规则禁止照抄键值与字段表;展示层 stripInternalFields 把字段行 / 字段表移进技术信息 - 开场建议按页面给 2 条完整问法、点了直接发;指令只留 /先给方案,/重跑 改成「重新运行」按钮 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
1 parent
dd0f6e2c09
commit
3692793dff
35 files changed
+1454
-1137
No files matched your search
@@ -1,7 +1,7 @@
|
||||
import { useQueryClient } from "@tanstack/react-query";
|
||||
import { type AgentRunStatus, EventType } from "@umt/contracts";
|
||||
import { ChevronDown, FlaskConical, ListChecks, RefreshCw, WifiOff } from "lucide-react";
|
||||
import { type ReactNode, useId, useRef, useState } from "react";
|
||||
import { Bot, ChevronDown, FlaskConical, ListChecks, RefreshCw, RotateCcw, WifiOff } from "lucide-react";
|
||||
import { type ReactNode, useId, useState } from "react";
|
||||
import { useNavigate } from "react-router";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
@@ -42,28 +42,21 @@ import {
|
||||
import { RunPlan } from "./agent-interaction.tsx";
|
||||
import { readDraft, runDraftKey, writeDraft } from "./agent-nav.ts";
|
||||
import { shortGoalTitle } from "./agent-store.ts";
|
||||
import { SubagentTree } from "./agent-subagent-tree.tsx";
|
||||
import { callCounts, sessionStatsParts, stepProgress } from "./conversation-model.ts";
|
||||
import {
|
||||
NEW_RUN_COMMANDS,
|
||||
PROPOSE_FIRST,
|
||||
PROPOSE_FIRST_MESSAGE,
|
||||
parseCommand,
|
||||
RERUN,
|
||||
RUN_COMMANDS,
|
||||
} from "./slash-commands.ts";
|
||||
import { ParentRunLink, SubagentChip, SubagentNodesContext } from "./agent-subagents.tsx";
|
||||
import { callCounts, sessionStatsParts, stepProgress, subagentViews } from "./conversation-model.ts";
|
||||
import { COMMANDS, PROPOSE_FIRST, PROPOSE_FIRST_MESSAGE, parseCommand } from "./slash-commands.ts";
|
||||
import { type AgentStreamState, useAgentStream } from "./use-agent-stream.ts";
|
||||
|
||||
/** 新目标的提交内容:`/先给方案` 开头 → proposeFirst(只作用于这一次) */
|
||||
export function goalFromInput(text: string): { goal: string; opts: AgentSessionOptions } | null {
|
||||
const { command, rest } = parseCommand(text, NEW_RUN_COMMANDS);
|
||||
const { command, rest } = parseCommand(text);
|
||||
if (!rest) return null;
|
||||
return { goal: rest, opts: { proposeFirst: command === PROPOSE_FIRST.name } };
|
||||
}
|
||||
|
||||
/**
|
||||
* 新目标输入:只有输入框和发送;`/先给方案` 让这一次先给方案。空态下面 3 个快捷目标,点一下填进输入框
|
||||
* (以空格或冒号结尾的是半句,光标停在末尾等补完)。上下文(当前页面的对象)显示成名称标签,可以去掉。
|
||||
* 新对话(抽屉和列表页是同一个组件):上面一句说明,下面开场建议和输入框。开场建议点一下直接发出;
|
||||
* `/先给方案` 让这一次先给方案。上下文(当前页面的对象)显示成输入框上的名称标签,可以去掉。
|
||||
*/
|
||||
export function GoalComposer({
|
||||
onSubmit,
|
||||
@@ -73,6 +66,7 @@ export function GoalComposer({
|
||||
defaultGoal = "",
|
||||
suggestions,
|
||||
draftKey,
|
||||
className,
|
||||
}: {
|
||||
onSubmit: (goal: string, opts: AgentSessionOptions) => void;
|
||||
pending: boolean;
|
||||
@@ -80,26 +74,17 @@ export function GoalComposer({
|
||||
context?: string | null;
|
||||
onClearContext?: () => void;
|
||||
defaultGoal?: string;
|
||||
/** 快捷目标:输入框为空时显示 */
|
||||
/** 开场建议:输入框为空时显示 */
|
||||
suggestions?: string[];
|
||||
/** 草稿记在这个键下(没有预填目标时恢复);发送成功后由调用方清掉 */
|
||||
draftKey?: string;
|
||||
className?: string;
|
||||
}) {
|
||||
const [goal, setGoalState] = useState(() => defaultGoal || readDraft(draftKey));
|
||||
const setGoal = (text: string) => {
|
||||
setGoalState(text);
|
||||
writeDraft(draftKey, text);
|
||||
};
|
||||
const box = useRef<HTMLDivElement>(null);
|
||||
const fill = (text: string) => {
|
||||
setGoal(text);
|
||||
requestAnimationFrame(() => {
|
||||
const ta = box.current?.querySelector("textarea");
|
||||
if (!ta) return;
|
||||
ta.focus();
|
||||
ta.setSelectionRange(text.length, text.length);
|
||||
});
|
||||
};
|
||||
const submit = (text: string) => {
|
||||
const g = goalFromInput(text);
|
||||
if (!g) {
|
||||
@@ -109,7 +94,32 @@ export function GoalComposer({
|
||||
onSubmit(g.goal, g.opts);
|
||||
};
|
||||
return (
|
||||
<div ref={box} className="flex flex-col gap-3">
|
||||
<div className={cn("flex min-h-0 flex-1 flex-col gap-3", className)}>
|
||||
<div className="flex flex-1 flex-col items-center justify-center gap-2 px-4 py-6 text-center">
|
||||
<span aria-hidden className="grid size-10 place-items-center rounded-full pill-selected">
|
||||
<Bot className="size-5" />
|
||||
</span>
|
||||
<p className="text-lead font-semibold text-foreground">有什么要我做的?</p>
|
||||
<p className="max-w-[36ch] text-caption text-muted-foreground">
|
||||
说一个目标就行,比如收一部电影、查一个下载为什么不动。需要批准的操作会先停下来问你。
|
||||
</p>
|
||||
</div>
|
||||
{suggestions?.length && !goal.trim() ? (
|
||||
<ul aria-label="开场建议" className="flex flex-wrap gap-2">
|
||||
{suggestions.map((s) => (
|
||||
<li key={s} className="min-w-0">
|
||||
<button
|
||||
type="button"
|
||||
disabled={pending}
|
||||
onClick={() => submit(s)}
|
||||
className="max-w-full cursor-pointer truncate rounded-full border border-border/70 px-3 py-1.5 text-left text-caption text-foreground/90 outline-none transition-colors duration-micro hover:bg-foreground/[0.04] hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
||||
>
|
||||
{s}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
<PromptInput
|
||||
value={goal}
|
||||
onValueChange={setGoal}
|
||||
@@ -121,25 +131,10 @@ export function GoalComposer({
|
||||
aria-label="目标"
|
||||
placeholder="例如:下载沙丘 2 的 4K 版本"
|
||||
submitLabel={pending ? "提交中" : "开始"}
|
||||
commands={NEW_RUN_COMMANDS}
|
||||
commands={COMMANDS}
|
||||
context={context}
|
||||
onClearContext={onClearContext}
|
||||
/>
|
||||
{suggestions?.length && !goal.trim() ? (
|
||||
<ul aria-label="快捷目标" className="flex flex-col gap-1.5">
|
||||
{suggestions.slice(0, 3).map((s) => (
|
||||
<li key={s}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => fill(s)}
|
||||
className="w-full cursor-pointer rounded-xl border border-border/70 px-3 py-2 text-left text-caption text-foreground/90 outline-none transition-colors duration-micro hover:bg-foreground/[0.04] hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{s.endsWith(" ") || s.endsWith(":") ? `${s.trimEnd()}…` : s}
|
||||
</button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -191,13 +186,11 @@ export function RunStatsLine({
|
||||
|
||||
export function RunOverviewBar({
|
||||
snapshot,
|
||||
status,
|
||||
live,
|
||||
active,
|
||||
className,
|
||||
}: {
|
||||
snapshot: RunSnapshot;
|
||||
status: AgentRunStatus;
|
||||
live: AgentStreamState;
|
||||
active: boolean;
|
||||
className?: string;
|
||||
@@ -208,8 +201,8 @@ export function RunOverviewBar({
|
||||
const steps = live.plan?.steps ?? [];
|
||||
const progress = stepProgress(steps);
|
||||
const failed = progress.failedStep;
|
||||
const family = (snapshot.children?.length ?? 0) > 0 || !!snapshot.parent;
|
||||
if (!steps.length && !family && !isRunActive(status)) return null;
|
||||
// 没有步骤清单就不显示(状态在顶部一行,子会话在会话流里)
|
||||
if (!steps.length) return null;
|
||||
const u = { ...r.usage, ...(live.usage ?? {}) };
|
||||
return (
|
||||
<section aria-label="会话概况" className={cn("shrink-0 rounded-xl border border-border/70", className)}>
|
||||
@@ -220,40 +213,28 @@ export function RunOverviewBar({
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
className="flex min-h-9 w-full min-w-0 cursor-pointer items-center gap-2 rounded-xl px-3 py-1 text-left text-meta outline-none transition-colors duration-micro hover:bg-foreground/[0.03] focus-visible:ring-2 focus-visible:ring-ring"
|
||||
>
|
||||
{isRunActive(status) ? <RunStatusBadge status={status} /> : null}
|
||||
{steps.length ? (
|
||||
<>
|
||||
<ListChecks
|
||||
aria-hidden
|
||||
className={cn("size-3.5 shrink-0 text-muted-foreground", failed && "text-destructive")}
|
||||
/>
|
||||
<span className="shrink-0 text-foreground tabular-nums">
|
||||
步骤 {progress.done}/{progress.total}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate text-muted-foreground">{progress.status}</span>
|
||||
</>
|
||||
) : (
|
||||
<span className="min-w-0 flex-1 truncate text-muted-foreground">
|
||||
{family ? `${snapshot.children?.length ?? 0} 个子会话` : ""}
|
||||
</span>
|
||||
)}
|
||||
<ListChecks
|
||||
aria-hidden
|
||||
className={cn("size-3.5 shrink-0 text-muted-foreground", failed && "text-destructive")}
|
||||
/>
|
||||
<span className="shrink-0 text-foreground tabular-nums">
|
||||
步骤 {progress.done}/{progress.total}
|
||||
</span>
|
||||
<span className="min-w-0 flex-1 truncate text-muted-foreground">{progress.status}</span>
|
||||
<ChevronDown aria-hidden className={cn("size-3.5 shrink-0", CHEVRON_CLASS, open && "rotate-180")} />
|
||||
</button>
|
||||
{steps.length ? (
|
||||
<div className="mx-3 mb-1.5 h-0.5 overflow-hidden rounded-full bg-foreground/[0.08]" aria-hidden>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full rounded-full transition-[width] duration-layout ease-standard motion-reduce:transition-none",
|
||||
failed ? "bg-destructive" : "bg-foreground/60",
|
||||
)}
|
||||
style={{ width: `${progress.pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
<div className="mx-3 mb-1.5 h-0.5 overflow-hidden rounded-full bg-foreground/[0.08]" aria-hidden>
|
||||
<div
|
||||
className={cn(
|
||||
"h-full rounded-full transition-[width] duration-layout ease-standard motion-reduce:transition-none",
|
||||
failed ? "bg-destructive" : "bg-foreground/60",
|
||||
)}
|
||||
style={{ width: `${progress.pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
<Disclosure id={id} open={open}>
|
||||
<div className="max-h-[50dvh] space-y-3 overflow-y-auto border-t border-border/60 p-3">
|
||||
<RunPlan plan={live.plan} active={active} fill />
|
||||
<SubagentTree snapshot={snapshot} className="p-2.5" />
|
||||
<p className="text-meta text-muted-foreground">
|
||||
{TRIGGER_LABEL[r.trigger]} · {formatDateTime(r.createdAt)}
|
||||
</p>
|
||||
@@ -281,8 +262,8 @@ export function RunOverviewBar({
|
||||
}
|
||||
|
||||
/**
|
||||
* 一次运行:顶部概况条 → 会话流(统一最大宽度)→ 底部输入区(只有输入框和发送 / 停止)。
|
||||
* 输入区的 `/重跑` 用同一个目标新开;`/先给方案` 让智能体先给方案。compact 用于抽屉。
|
||||
* 一次运行(抽屉和会话页是同一个组件,compact = 抽屉):顶部一行状态与统计 → 步骤清单(有才显示)→
|
||||
* 会话流(子会话卡片在派出的位置)→ 底部输入区。结束后顶部有「重新运行」;输入框只有 `/先给方案` 一个指令。
|
||||
*/
|
||||
export function AgentRunPanel({
|
||||
runId,
|
||||
@@ -296,7 +277,7 @@ export function AgentRunPanel({
|
||||
className?: string;
|
||||
/** 统计行右侧的操作(会话页的「在侧边打开」) */
|
||||
actions?: ReactNode;
|
||||
/** /重跑 之后:缺省跳到新会话页;抽屉里改成在抽屉内打开 */
|
||||
/** 重新运行之后:缺省跳到新会话页;抽屉里改成在抽屉内打开 */
|
||||
onCreated?: (runId: string) => void;
|
||||
}) {
|
||||
const run = useAgentRun(runId);
|
||||
@@ -354,21 +335,14 @@ export function AgentRunPanel({
|
||||
const ended = !isRunActive(r.status);
|
||||
const created = onCreated ?? ((id: string) => navigate(`/console/agent/${id}${agentMockEnabled() ? "?mock=1" : ""}`));
|
||||
|
||||
const rerun = () =>
|
||||
create.mutate(
|
||||
{ goal: r.goal, proposeFirst: r.scope.proposeFirst === true },
|
||||
{ onSuccess: (n) => created(n.id), onError: (e) => toast.error(errorMessage(e)) },
|
||||
);
|
||||
|
||||
const send = (text: string) => {
|
||||
const { command, rest } = parseCommand(text, RUN_COMMANDS);
|
||||
if (command === RERUN.name) {
|
||||
create.mutate(
|
||||
{ goal: r.goal, proposeFirst: r.scope.proposeFirst === true },
|
||||
{
|
||||
onSuccess: (n) => {
|
||||
setMsg("");
|
||||
created(n.id);
|
||||
},
|
||||
onError: (e) => toast.error(errorMessage(e)),
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
const { command, rest } = parseCommand(text);
|
||||
const body = command === PROPOSE_FIRST.name ? `${PROPOSE_FIRST_MESSAGE}${rest ? `\n${rest}` : ""}` : text;
|
||||
append.mutate(
|
||||
{ id: r.id, text: body },
|
||||
@@ -383,136 +357,154 @@ export function AgentRunPanel({
|
||||
};
|
||||
|
||||
const column = "mx-auto w-full max-w-[88ch]";
|
||||
const subagents = subagentViews(live.items);
|
||||
const nodes = new Map((run.data.children ?? []).map((c) => [c.id, c] as const));
|
||||
// 只标需要注意的状态;正常结束不加徽章(结果在会话末尾)
|
||||
const showBadge = shownStatus !== "succeeded";
|
||||
|
||||
return (
|
||||
<div className={cn("flex min-h-0 flex-col gap-3", className)}>
|
||||
<RunAnnouncer status={shownStatus} finished={live.finished} />
|
||||
{agentMockEnabled() && !compact ? (
|
||||
<div className={column}>
|
||||
<StatusBadge tone="warning">
|
||||
<FlaskConical className="size-3" />
|
||||
演示数据
|
||||
</StatusBadge>
|
||||
</div>
|
||||
) : null}
|
||||
{actions ? (
|
||||
<div className={cn(column, "-my-1 flex min-w-0 items-center gap-2")}>
|
||||
<RunStatsLine snapshot={run.data} live={live} className="min-w-0 flex-1" />
|
||||
<div className="flex shrink-0 items-center gap-1">{actions}</div>
|
||||
</div>
|
||||
) : (
|
||||
<RunStatsLine snapshot={run.data} live={live} className={column} />
|
||||
)}
|
||||
<RunOverviewBar
|
||||
snapshot={run.data}
|
||||
status={shownStatus}
|
||||
live={live}
|
||||
active={streaming || r.status === "waiting"}
|
||||
className={column}
|
||||
/>
|
||||
|
||||
{streaming && sseStatus === "reconnecting" ? (
|
||||
<SubagentNodesContext.Provider value={nodes}>
|
||||
<div className={cn("flex min-h-0 flex-col gap-3", className)}>
|
||||
<RunAnnouncer status={shownStatus} finished={live.finished} />
|
||||
{agentMockEnabled() && !compact ? (
|
||||
<div className={column}>
|
||||
<StatusBadge tone="warning">
|
||||
<FlaskConical className="size-3" />
|
||||
演示数据
|
||||
</StatusBadge>
|
||||
</div>
|
||||
) : null}
|
||||
{run.data.parent ? (
|
||||
<div className={column}>
|
||||
<ParentRunLink parent={run.data.parent} />
|
||||
</div>
|
||||
) : null}
|
||||
{/* 顶部一行:状态 · 统计 · 子会话 · 操作;窄的时候折行 */}
|
||||
<div
|
||||
role="status"
|
||||
className={cn(
|
||||
column,
|
||||
"flex items-center gap-2 rounded-xl border border-warning/40 bg-warning/10 px-3 py-2 text-caption text-warning",
|
||||
)}
|
||||
data-slot="run-header"
|
||||
className={cn(column, "-my-1 flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1.5")}
|
||||
>
|
||||
<WifiOff aria-hidden className="size-4 shrink-0" />
|
||||
<span className="min-w-0 flex-1">实时连接中断,正在重连。</span>
|
||||
{showBadge ? <RunStatusBadge status={shownStatus} /> : null}
|
||||
<RunStatsLine snapshot={run.data} live={live} className="min-w-0 flex-1 basis-48" />
|
||||
<div className="ml-auto flex shrink-0 items-center gap-1">
|
||||
<SubagentChip views={subagents} />
|
||||
{ended ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
aria-label="用同一个目标重新运行"
|
||||
title="用同一个目标重新运行"
|
||||
disabled={create.isPending}
|
||||
onClick={rerun}
|
||||
>
|
||||
<RotateCcw />
|
||||
</Button>
|
||||
) : null}
|
||||
{actions}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{live.error ? (
|
||||
<div
|
||||
role="alert"
|
||||
className={cn(
|
||||
column,
|
||||
"flex flex-wrap items-center gap-2 rounded-xl border border-destructive/40 bg-destructive/10 px-3 py-2 text-caption text-destructive",
|
||||
)}
|
||||
>
|
||||
<span className="min-w-0 flex-1 break-words">事件读取失败:{live.error}</span>
|
||||
<Button size="xs" variant="outline" onClick={live.retry}>
|
||||
<RefreshCw />
|
||||
重试
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<MessageScroller
|
||||
className={cn("min-h-48 flex-1", compact && "min-h-40")}
|
||||
viewportClassName="pr-1"
|
||||
contentClassName={column}
|
||||
busy={streaming}
|
||||
label="智能体会话记录"
|
||||
>
|
||||
<AgentConversation
|
||||
run={r}
|
||||
<RunOverviewBar
|
||||
snapshot={run.data}
|
||||
live={live}
|
||||
knownNames={[
|
||||
...(run.data.children ?? []).map((c) => [c.id, shortGoalTitle(c.goal, 24)] as [string, string]),
|
||||
...(run.data.parent
|
||||
? [[run.data.parent.id, shortGoalTitle(run.data.parent.goal, 24)] as [string, string]]
|
||||
: []),
|
||||
]}
|
||||
audit={audit.data}
|
||||
userMessages={messages.map((m) => ({ at: m.createdAt, text: m.text, pending: m.status === "pending" }))}
|
||||
after={
|
||||
<>
|
||||
{approvals.items
|
||||
.filter((a) => !inline.has(a.id) && a.status === "pending")
|
||||
.map((a) => (
|
||||
<ApprovalItem key={a.id} approval={a} />
|
||||
))}
|
||||
{finished ? (
|
||||
<ResultLine
|
||||
run={r}
|
||||
report={report}
|
||||
live={live}
|
||||
subagents={(run.data.children ?? []).filter((c) => c.parentRunId === r.id).length}
|
||||
showCost={false}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
active={streaming || r.status === "waiting"}
|
||||
className={column}
|
||||
/>
|
||||
|
||||
{streaming && sseStatus === "reconnecting" ? (
|
||||
<div
|
||||
role="status"
|
||||
className={cn(
|
||||
column,
|
||||
"flex items-center gap-2 rounded-xl border border-warning/40 bg-warning/10 px-3 py-2 text-caption text-warning",
|
||||
)}
|
||||
>
|
||||
<WifiOff aria-hidden className="size-4 shrink-0" />
|
||||
<span className="min-w-0 flex-1">实时连接中断,正在重连。</span>
|
||||
</div>
|
||||
) : null}
|
||||
{live.error ? (
|
||||
<div
|
||||
role="alert"
|
||||
className={cn(
|
||||
column,
|
||||
"flex flex-wrap items-center gap-2 rounded-xl border border-destructive/40 bg-destructive/10 px-3 py-2 text-caption text-destructive",
|
||||
)}
|
||||
>
|
||||
<span className="min-w-0 flex-1 break-words">事件读取失败:{live.error}</span>
|
||||
<Button size="xs" variant="outline" onClick={live.retry}>
|
||||
<RefreshCw />
|
||||
重试
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<MessageScroller
|
||||
className={cn("min-h-48 flex-1", compact && "min-h-40")}
|
||||
viewportClassName="pr-1"
|
||||
contentClassName={column}
|
||||
busy={streaming}
|
||||
label="智能体会话记录"
|
||||
>
|
||||
<AgentConversation
|
||||
run={r}
|
||||
live={live}
|
||||
knownNames={[
|
||||
...(run.data.children ?? []).map((c) => [c.id, shortGoalTitle(c.goal, 24)] as [string, string]),
|
||||
...(run.data.parent
|
||||
? [[run.data.parent.id, shortGoalTitle(run.data.parent.goal, 24)] as [string, string]]
|
||||
: []),
|
||||
]}
|
||||
audit={audit.data}
|
||||
userMessages={messages.map((m) => ({ at: m.createdAt, text: m.text, pending: m.status === "pending" }))}
|
||||
after={
|
||||
<>
|
||||
{approvals.items
|
||||
.filter((a) => !inline.has(a.id) && a.status === "pending")
|
||||
.map((a) => (
|
||||
<ApprovalItem key={a.id} approval={a} />
|
||||
))}
|
||||
{finished ? <ResultLine run={r} report={report} live={live} showCost={false} /> : null}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</MessageScroller>
|
||||
|
||||
<div className={cn(column, "shrink-0")}>
|
||||
<PromptInput
|
||||
value={msg}
|
||||
onValueChange={setMsg}
|
||||
onSubmit={send}
|
||||
pending={append.isPending || create.isPending}
|
||||
loading={streaming}
|
||||
onStop={() => setConfirmStop(true)}
|
||||
stopLabel={abort.isPending ? "停止中…" : "停止"}
|
||||
minRows={1}
|
||||
maxRows={6}
|
||||
maxLength={8000}
|
||||
aria-label="发消息"
|
||||
placeholder={ended ? "继续对话" : "补充或纠正"}
|
||||
commands={COMMANDS}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
open={confirmStop}
|
||||
onOpenChange={setConfirmStop}
|
||||
title="停止这个会话?"
|
||||
description="后续步骤不再执行。已完成的操作不会撤销,已提交的下载会继续。"
|
||||
confirmLabel="停止会话"
|
||||
destructive
|
||||
onConfirm={() =>
|
||||
abort.mutateAsync(r.id).then(
|
||||
() => toast.success("正在停止"),
|
||||
(e) => {
|
||||
toast.error(errorMessage(e));
|
||||
throw e;
|
||||
},
|
||||
)
|
||||
}
|
||||
/>
|
||||
</MessageScroller>
|
||||
|
||||
<div className={cn(column, "shrink-0")}>
|
||||
<PromptInput
|
||||
value={msg}
|
||||
onValueChange={setMsg}
|
||||
onSubmit={send}
|
||||
pending={append.isPending || create.isPending}
|
||||
loading={streaming}
|
||||
onStop={() => setConfirmStop(true)}
|
||||
stopLabel={abort.isPending ? "停止中…" : "停止"}
|
||||
minRows={1}
|
||||
maxRows={6}
|
||||
maxLength={8000}
|
||||
aria-label="发消息"
|
||||
placeholder={ended ? "继续对话,输入 / 查看指令" : "补充或纠正"}
|
||||
commands={RUN_COMMANDS}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
open={confirmStop}
|
||||
onOpenChange={setConfirmStop}
|
||||
title="停止这个会话?"
|
||||
description="后续步骤不再执行。已完成的操作不会撤销,已提交的下载会继续。"
|
||||
confirmLabel="停止会话"
|
||||
destructive
|
||||
onConfirm={() =>
|
||||
abort.mutateAsync(r.id).then(
|
||||
() => toast.success("正在停止"),
|
||||
(e) => {
|
||||
toast.error(errorMessage(e));
|
||||
throw e;
|
||||
},
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</SubagentNodesContext.Provider>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user