mirror of
https://github.com/multica-ai/multica.git
synced 2026-07-04 21:09:56 +02:00
Compare commits
2 Commits
agent/lamb
...
agent/walt
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
587e952d45 | ||
|
|
df9538dc0c |
@@ -0,0 +1,72 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { cleanup, screen } from "@testing-library/react";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { AgentTask } from "@multica/core/types";
|
||||
import { renderWithI18n } from "../../test/i18n";
|
||||
|
||||
const mockState = vi.hoisted(() => ({
|
||||
taskMessagesOptions: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@multica/core/chat/queries", () => ({
|
||||
taskMessagesOptions: mockState.taskMessagesOptions,
|
||||
}));
|
||||
|
||||
vi.mock("../../common/actor-avatar", () => ({
|
||||
ActorAvatar: () => <span data-testid="actor-avatar" />,
|
||||
}));
|
||||
|
||||
vi.mock("../../common/task-transcript", () => ({
|
||||
TranscriptButton: ({ title }: { title?: string }) => (
|
||||
<button type="button">{title ?? "Transcript"}</button>
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock("./terminate-task-confirm-dialog", () => ({
|
||||
TerminateTaskConfirmDialog: () => null,
|
||||
}));
|
||||
|
||||
import { ActiveTaskRow } from "./execution-log-section";
|
||||
|
||||
function makeTask(overrides: Partial<AgentTask> = {}): AgentTask {
|
||||
return {
|
||||
id: "task-1",
|
||||
agent_id: "agent-1",
|
||||
runtime_id: "runtime-1",
|
||||
issue_id: "issue-1",
|
||||
status: "running",
|
||||
priority: 0,
|
||||
dispatched_at: null,
|
||||
started_at: "2026-06-08T08:00:00Z",
|
||||
completed_at: null,
|
||||
result: null,
|
||||
error: null,
|
||||
created_at: "2026-06-08T08:00:00Z",
|
||||
trigger_summary: "Started from comment",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(new Date("2026-06-08T08:05:04Z"));
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
describe("ActiveTaskRow", () => {
|
||||
it("renders running status as elapsed time only", () => {
|
||||
renderWithI18n(<ActiveTaskRow task={makeTask()} issueId="issue-1" />);
|
||||
|
||||
expect(screen.getByText("5m 04s")).toBeInTheDocument();
|
||||
expect(screen.queryByText(/events?/i)).not.toBeInTheDocument();
|
||||
expect(screen.getByText("Started from comment")).toBeInTheDocument();
|
||||
expect(screen.getByText("View transcript")).toBeInTheDocument();
|
||||
expect(mockState.taskMessagesOptions).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -15,9 +15,7 @@ import {
|
||||
} from "@multica/ui/components/ui/tooltip";
|
||||
import { ActorAvatar } from "../../common/actor-avatar";
|
||||
import { formatDuration } from "../../agents/components/agent-activity-hover-content";
|
||||
import { TranscriptButton, buildTimeline } from "../../common/task-transcript";
|
||||
import { taskMessagesOptions } from "@multica/core/chat/queries";
|
||||
import { RunningStat } from "./running-stat";
|
||||
import { TranscriptButton } from "../../common/task-transcript";
|
||||
import { failureReasonLabel } from "../../agents/components/tabs/task-failure";
|
||||
import { useT } from "../../i18n";
|
||||
import { TerminateTaskConfirmDialog } from "./terminate-task-confirm-dialog";
|
||||
@@ -245,15 +243,10 @@ function useStatusLabel(status: AgentTask["status"]): string {
|
||||
}
|
||||
}
|
||||
|
||||
// One active (running / queued / dispatched / parked) task row. Exported so
|
||||
// the issue-detail header live-chip popover renders the exact same row as
|
||||
// this panel — same trigger text, same status treatment, same hover-reveal
|
||||
// Logs/Stop. The popover hosting it must use `keepMounted` so this row (and
|
||||
// its internal confirm dialog) survives the popover closing on Stop click.
|
||||
// Running rows read the shared per-task message cache (taskMessagesOptions,
|
||||
// kept live by useRealtimeSync's global task:message handler) so every surface
|
||||
// — this panel, the header-chip popover, and the transcript dialog — shows the
|
||||
// same live "N events (elapsed)" and the same streaming Logs from one source.
|
||||
// One active (running / queued / dispatched / parked) task row. Running rows
|
||||
// keep status to a single live elapsed timer; transcript and stop stay available
|
||||
// as hover actions. Transcript content lazy-loads on click via TranscriptButton,
|
||||
// so the row no longer fetches task messages just to render a count.
|
||||
export function ActiveTaskRow({
|
||||
task,
|
||||
issueId,
|
||||
@@ -268,15 +261,6 @@ export function ActiveTaskRow({
|
||||
const label = useStatusLabel(task.status);
|
||||
const trigger = useTriggerText(task);
|
||||
|
||||
// Live message stream for this task — only fetched while running. The shared
|
||||
// cache means the panel row, the popover row, and the chip all dedupe to one
|
||||
// fetch + one WS-maintained entry.
|
||||
const { data: msgs } = useQuery({
|
||||
...taskMessagesOptions(task.id),
|
||||
enabled: task.status === "running",
|
||||
});
|
||||
const items = useMemo(() => (msgs ? buildTimeline(msgs) : undefined), [msgs]);
|
||||
|
||||
// Running rows show a live-ticking elapsed timer (the ticking digits carry
|
||||
// "alive", the duration carries "how long"). Only running rows tick.
|
||||
const [now, setNow] = useState(() => Date.now());
|
||||
@@ -320,7 +304,7 @@ export function ActiveTaskRow({
|
||||
<RowStatus title={label}>
|
||||
{task.status === "running" ? (
|
||||
<>
|
||||
<RunningStat eventCount={msgs?.length ?? 0} elapsed={elapsed} />
|
||||
<span className="text-info tabular-nums">{elapsed}</span>
|
||||
<span className="sr-only">{label}</span>
|
||||
</>
|
||||
) : (
|
||||
@@ -332,8 +316,7 @@ export function ActiveTaskRow({
|
||||
<TranscriptButton
|
||||
task={task}
|
||||
agentName=""
|
||||
isLive
|
||||
items={items}
|
||||
isLive={task.status === "running"}
|
||||
title={t(($) => $.execution_log.transcript_tooltip)}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
// @vitest-environment jsdom
|
||||
|
||||
import { cleanup, screen } from "@testing-library/react";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { AgentTask } from "@multica/core/types";
|
||||
import { renderWithI18n } from "../../test/i18n";
|
||||
|
||||
const mockState = vi.hoisted(() => ({
|
||||
snapshot: [] as unknown[],
|
||||
taskMessagesOptions: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@multica/core/hooks", () => ({
|
||||
useWorkspaceId: () => "ws-1",
|
||||
}));
|
||||
|
||||
vi.mock("@multica/core/workspace/hooks", () => ({
|
||||
useActorName: () => ({
|
||||
getActorName: (_type: string, id: string) =>
|
||||
({
|
||||
"agent-1": "Walt",
|
||||
"agent-2": "Gus",
|
||||
})[id] ?? "Unknown Agent",
|
||||
getActorInitials: (_type: string, id: string) =>
|
||||
({
|
||||
"agent-1": "WA",
|
||||
"agent-2": "GU",
|
||||
})[id] ?? "UA",
|
||||
getActorAvatarUrl: () => null,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@multica/core/chat/queries", () => ({
|
||||
taskMessagesOptions: mockState.taskMessagesOptions,
|
||||
}));
|
||||
|
||||
vi.mock("@tanstack/react-query", async () => {
|
||||
const actual =
|
||||
await vi.importActual<typeof import("@tanstack/react-query")>(
|
||||
"@tanstack/react-query",
|
||||
);
|
||||
|
||||
return {
|
||||
...actual,
|
||||
useQuery: (opts: { queryKey?: readonly unknown[] }) => {
|
||||
if (opts.queryKey?.[2] === "agent-task-snapshot") {
|
||||
return { data: mockState.snapshot };
|
||||
}
|
||||
return { data: undefined };
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
import { IssueAgentHeaderChip } from "./issue-agent-header-chip";
|
||||
|
||||
function makeTask(overrides: Partial<AgentTask>): AgentTask {
|
||||
return {
|
||||
id: "task-1",
|
||||
agent_id: "agent-1",
|
||||
runtime_id: "runtime-1",
|
||||
issue_id: "issue-1",
|
||||
status: "running",
|
||||
priority: 0,
|
||||
dispatched_at: null,
|
||||
started_at: "2026-06-08T08:00:00Z",
|
||||
completed_at: null,
|
||||
result: null,
|
||||
error: null,
|
||||
created_at: "2026-06-08T08:00:00Z",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
cleanup();
|
||||
vi.clearAllMocks();
|
||||
mockState.snapshot = [];
|
||||
});
|
||||
|
||||
describe("IssueAgentHeaderChip", () => {
|
||||
it("shows the active agent name without event count or elapsed time", () => {
|
||||
mockState.snapshot = [makeTask({})];
|
||||
|
||||
renderWithI18n(<IssueAgentHeaderChip issueId="issue-1" />);
|
||||
|
||||
expect(
|
||||
screen.getByRole("status", { name: "Walt is working" }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("Walt is working")).toBeInTheDocument();
|
||||
expect(screen.queryByText(/events?/i)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(/\d+[smh]/i)).not.toBeInTheDocument();
|
||||
expect(mockState.taskMessagesOptions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses the concise multi-agent working label", () => {
|
||||
mockState.snapshot = [
|
||||
makeTask({ id: "task-1", agent_id: "agent-1" }),
|
||||
makeTask({ id: "task-2", agent_id: "agent-2" }),
|
||||
];
|
||||
|
||||
renderWithI18n(<IssueAgentHeaderChip issueId="issue-1" />);
|
||||
|
||||
expect(
|
||||
screen.getByRole("status", { name: "2 agents working" }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText("2 agents working")).toBeInTheDocument();
|
||||
expect(mockState.taskMessagesOptions).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses the requested Chinese single-agent copy", () => {
|
||||
mockState.snapshot = [makeTask({})];
|
||||
|
||||
renderWithI18n(<IssueAgentHeaderChip issueId="issue-1" />, {
|
||||
locale: "zh-Hans",
|
||||
});
|
||||
|
||||
expect(screen.getByText("Walt 在工作")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render for inactive or unrelated tasks", () => {
|
||||
mockState.snapshot = [
|
||||
makeTask({
|
||||
id: "task-done",
|
||||
status: "completed",
|
||||
completed_at: "2026-06-08T08:05:00Z",
|
||||
}),
|
||||
makeTask({ id: "task-other", issue_id: "issue-2" }),
|
||||
];
|
||||
|
||||
renderWithI18n(<IssueAgentHeaderChip issueId="issue-1" />);
|
||||
|
||||
expect(screen.queryByRole("status")).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,21 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { memo, useEffect, useMemo, useState } from "react";
|
||||
import { memo, useMemo } from "react";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Clock } from "lucide-react";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@multica/ui/components/ui/popover";
|
||||
import { useWorkspaceId } from "@multica/core/hooks";
|
||||
import { useActorName } from "@multica/core/workspace/hooks";
|
||||
import { agentTaskSnapshotOptions } from "@multica/core/agents";
|
||||
import { taskMessagesOptions } from "@multica/core/chat/queries";
|
||||
import type { AgentTask } from "@multica/core/types";
|
||||
import { AgentAvatarStack } from "../../agents/components/agent-avatar-stack";
|
||||
import { formatDuration } from "../../agents/components/agent-activity-hover-content";
|
||||
import { ActiveTaskRow } from "./execution-log-section";
|
||||
import { RunningStat } from "./running-stat";
|
||||
import { useT } from "../../i18n";
|
||||
|
||||
// Per-issue "is an agent working on this right now?" chip for the issue
|
||||
@@ -28,16 +19,13 @@ import { useT } from "../../i18n";
|
||||
// list-row IssueAgentActivityIndicator, so the chip is always consistent
|
||||
// with those surfaces and costs zero extra network.
|
||||
//
|
||||
// Collapsed display — the avatar stack carries how many agents, the text
|
||||
// always carries one elapsed time, so a multi-agent chip reads "N heads +
|
||||
// how long" rather than a redundant count next to countable heads:
|
||||
// - running → avatar(s) + blue elapsed of the longest-running task
|
||||
// - queued only → half-opacity avatar(s) + clock + muted longest wait
|
||||
// Collapsed display stays intentionally shallow:
|
||||
// - one active agent → avatar + "{name} is working"
|
||||
// - multiple agents → avatar stack + "N agents working"
|
||||
// - queued only → same copy, half-opacity avatars / muted text
|
||||
//
|
||||
// Click opens a Popover listing every active task with the SAME row as the
|
||||
// right-panel ExecutionLogSection (ActiveTaskRow) — trigger text + status,
|
||||
// with Logs/Stop revealed on row hover. The popover is `keepMounted` so the
|
||||
// row's internal confirm dialog survives the popover closing on Stop click.
|
||||
// Events, elapsed time, transcript, and stop controls belong to the right
|
||||
// panel ExecutionLogSection. The header is a scan-only status segment.
|
||||
|
||||
interface IssueAgentHeaderChipProps {
|
||||
issueId: string;
|
||||
@@ -68,113 +56,53 @@ export const IssueAgentHeaderChip = memo(function IssueAgentHeaderChip({
|
||||
return { running, queued };
|
||||
}, [snapshot, issueId]);
|
||||
|
||||
// No active work → render nothing (and crucially, no `now` ticker). The
|
||||
// 1s elapsed tick only runs while the ActiveChip is mounted.
|
||||
// No active work → render nothing. Header details stay out of this component;
|
||||
// the right panel owns per-task timing, messages, transcript, and stop controls.
|
||||
if (running.length === 0 && queued.length === 0) return null;
|
||||
|
||||
return <ActiveChip issueId={issueId} running={running} queued={queued} />;
|
||||
return <ActiveChip running={running} queued={queued} />;
|
||||
});
|
||||
|
||||
interface ActiveChipProps {
|
||||
issueId: string;
|
||||
running: AgentTask[];
|
||||
queued: AgentTask[];
|
||||
}
|
||||
|
||||
function ActiveChip({ issueId, running, queued }: ActiveChipProps) {
|
||||
function ActiveChip({ running, queued }: ActiveChipProps) {
|
||||
const { t } = useT("issues");
|
||||
|
||||
// Tick once per second so the collapsed single-agent elapsed stays live.
|
||||
// Only mounted while there's active work, so it costs nothing at rest.
|
||||
const [now, setNow] = useState(() => Date.now());
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setNow(Date.now()), 1000);
|
||||
return () => clearInterval(id);
|
||||
}, []);
|
||||
const { getActorName } = useActorName();
|
||||
|
||||
const activeTasks = [...running, ...queued];
|
||||
const agentIds = [...new Set(activeTasks.map((task) => task.agent_id))];
|
||||
const anyRunning = running.length > 0;
|
||||
const isSingle = agentIds.length === 1;
|
||||
|
||||
// Single-agent event count comes from the shared per-task message cache —
|
||||
// the same entry the popover row and Logs read, kept live by
|
||||
// useRealtimeSync. Only the single-running case shows a number, so we only
|
||||
// query that one task; multi shows "N working".
|
||||
const singleRunningTaskId =
|
||||
isSingle && anyRunning ? (running[0]?.id ?? "") : "";
|
||||
const { data: singleMsgs } = useQuery({
|
||||
...taskMessagesOptions(singleRunningTaskId),
|
||||
enabled: singleRunningTaskId !== "",
|
||||
});
|
||||
|
||||
// One elapsed time = how long work has been going on this issue. When
|
||||
// anything is running, anchor on the longest-running task (earliest start =
|
||||
// the "is something stuck?" signal); when all queued, the longest wait.
|
||||
// The relevant bucket is always non-empty (ActiveChip only mounts with ≥1
|
||||
// active task), so the reduce has a safe seed.
|
||||
const anchorOf = (task: AgentTask) =>
|
||||
task.status === "running"
|
||||
? (task.started_at ?? task.dispatched_at ?? task.created_at)
|
||||
: task.created_at;
|
||||
const bucket = anyRunning ? running : queued;
|
||||
const leadFrom = bucket
|
||||
.map(anchorOf)
|
||||
.reduce((a, b) => (new Date(b).getTime() < new Date(a).getTime() ? b : a));
|
||||
const elapsed = formatDuration(leadFrom, now);
|
||||
const label = isSingle
|
||||
? t(($) => $.agent_live.is_working, {
|
||||
name: getActorName("agent", agentIds[0] ?? ""),
|
||||
})
|
||||
: t(($) => $.agent_activity.hover_header, {
|
||||
count: agentIds.length,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-1">
|
||||
<Popover>
|
||||
<PopoverTrigger
|
||||
render={
|
||||
<button
|
||||
type="button"
|
||||
aria-label={t(($) => $.agent_activity.hover_header, {
|
||||
count: activeTasks.length,
|
||||
})}
|
||||
className="flex h-7 items-center gap-1.5 rounded-md px-1.5 text-muted-foreground outline-none transition-colors hover:bg-accent/60 focus-visible:ring-2 focus-visible:ring-ring"
|
||||
/>
|
||||
}
|
||||
<div
|
||||
role="status"
|
||||
aria-label={label}
|
||||
className="flex h-7 max-w-[11rem] items-center gap-1.5 rounded-md px-1.5 text-muted-foreground"
|
||||
>
|
||||
<AgentAvatarStack
|
||||
agentIds={agentIds}
|
||||
size={18}
|
||||
max={3}
|
||||
opacity={anyRunning ? "full" : "half"}
|
||||
/>
|
||||
<span
|
||||
className={`min-w-0 truncate text-xs ${anyRunning ? "text-info" : "text-muted-foreground"}`}
|
||||
>
|
||||
<AgentAvatarStack
|
||||
agentIds={agentIds}
|
||||
size={18}
|
||||
max={3}
|
||||
opacity={anyRunning ? "full" : "half"}
|
||||
/>
|
||||
{!isSingle ? (
|
||||
// Multiple agents → "N working" (matches the workspace chip);
|
||||
// per-agent time/events live in the popover rows below.
|
||||
<span
|
||||
className={`text-xs ${anyRunning ? "text-info" : "text-muted-foreground"}`}
|
||||
>
|
||||
{agentIds.length} {t(($) => $.agent_activity.chip_label)}
|
||||
</span>
|
||||
) : anyRunning ? (
|
||||
// Single running → events (primary), elapsed in muted parens.
|
||||
<RunningStat eventCount={singleMsgs?.length ?? 0} elapsed={elapsed} />
|
||||
) : (
|
||||
// Single queued/parked → clock + wait time.
|
||||
<span className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Clock className="h-3 w-3 shrink-0" />
|
||||
<span className="tabular-nums">{elapsed}</span>
|
||||
</span>
|
||||
)}
|
||||
</PopoverTrigger>
|
||||
<PopoverContent align="end" keepMounted className="w-80">
|
||||
<div className="text-xs font-medium text-muted-foreground">
|
||||
{t(($) => $.agent_activity.hover_header, {
|
||||
count: activeTasks.length,
|
||||
})}
|
||||
</div>
|
||||
<div className="flex flex-col gap-0.5">
|
||||
{activeTasks.map((task) => (
|
||||
<ActiveTaskRow key={task.id} task={task} issueId={issueId} />
|
||||
))}
|
||||
</div>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
{label}
|
||||
</span>
|
||||
</div>
|
||||
{/* Separator from the action buttons — the chip is a status segment,
|
||||
not another button, so a hairline keeps the two groups legible. */}
|
||||
<span className="h-4 w-px bg-border" aria-hidden="true" />
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useT } from "../../i18n";
|
||||
|
||||
// Shared "running" stat for the header chip and the execution-log / popover
|
||||
// rows so all three read identically: live event count (primary, info blue)
|
||||
// followed by the elapsed time in muted parens — e.g. "58 events (2m45s)".
|
||||
// Both the count and the elapsed are live (event count via the shared
|
||||
// task-messages cache, elapsed via the caller's 1s tick).
|
||||
export function RunningStat({
|
||||
eventCount,
|
||||
elapsed,
|
||||
}: {
|
||||
eventCount: number;
|
||||
elapsed: string;
|
||||
}) {
|
||||
const { t } = useT("issues");
|
||||
return (
|
||||
<span className="flex items-center gap-1 text-xs tabular-nums">
|
||||
<span className="text-info">
|
||||
{t(($) => $.agent_live.event_count, { count: eventCount })}
|
||||
</span>
|
||||
<span className="text-muted-foreground">({elapsed})</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -307,8 +307,6 @@
|
||||
"fallback_name": "Agent",
|
||||
"tool_count_one": "{{count}} tool",
|
||||
"tool_count_other": "{{count}} tools",
|
||||
"event_count_one": "{{count}} event",
|
||||
"event_count_other": "{{count}} events",
|
||||
"transcript_button": "View transcript",
|
||||
"stop_button": "Stop",
|
||||
"stop_tooltip": "Stop agent",
|
||||
|
||||
@@ -295,7 +295,6 @@
|
||||
"queued_elapsed_prefix": "{{elapsed}} 待機中",
|
||||
"fallback_name": "エージェント",
|
||||
"tool_count_other": "ツール {{count}} 件",
|
||||
"event_count_other": "イベント {{count}} 件",
|
||||
"transcript_button": "トランスクリプトを表示",
|
||||
"stop_button": "停止",
|
||||
"stop_tooltip": "エージェントを停止",
|
||||
|
||||
@@ -307,8 +307,6 @@
|
||||
"fallback_name": "에이전트",
|
||||
"tool_count_one": "도구 {{count}}개",
|
||||
"tool_count_other": "도구 {{count}}개",
|
||||
"event_count_one": "이벤트 {{count}}개",
|
||||
"event_count_other": "이벤트 {{count}}개",
|
||||
"transcript_button": "트랜스크립트 보기",
|
||||
"stop_button": "중지",
|
||||
"stop_tooltip": "에이전트 중지",
|
||||
|
||||
@@ -294,13 +294,12 @@
|
||||
"filter_active_label": "正在查看工作中的智能体"
|
||||
},
|
||||
"agent_live": {
|
||||
"is_working": "{{name}} 正在处理",
|
||||
"is_working": "{{name}} 在工作",
|
||||
"is_queued": "{{name}} 排队中",
|
||||
"is_waiting_local_directory": "{{name}} 正在等待本地目录",
|
||||
"queued_elapsed_prefix": "已排队 {{elapsed}}",
|
||||
"fallback_name": "智能体",
|
||||
"tool_count_other": "{{count}} 次工具调用",
|
||||
"event_count_other": "{{count}} 个事件",
|
||||
"transcript_button": "查看记录",
|
||||
"stop_button": "停止",
|
||||
"stop_tooltip": "停止智能体",
|
||||
|
||||
Reference in New Issue
Block a user