Q158: run history and run output come from the database, not the snapshot window
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a075f931d5
commit
9d166e3fc9
18 files changed
+222
-35
No files matched your search
@@ -96,7 +96,7 @@ export function App() {
|
||||
office: <OfficePage {...pageProps} />,
|
||||
settings: <SettingsPage {...pageProps} />,
|
||||
}[route];
|
||||
const pageKey = focus ? `${route}:${focus.kind}:${focus.id}` : route;
|
||||
const pageKey = `${snapshot.activeOrgId}:${route}${focus ? `:${focus.kind}:${focus.id}` : ""}`;
|
||||
|
||||
return (
|
||||
<AvatarDirectoryContext.Provider value={avatarDirectory}>
|
||||
|
||||
@@ -52,6 +52,8 @@ export const zh: Record<string, string> = {
|
||||
"Showing {shown} of {total}": "显示 {total} 项中的 {shown} 项",
|
||||
"Show earlier decisions": "显示更早的决定",
|
||||
"Cancel run": "取消运行",
|
||||
"Show older runs": "显示更早的运行",
|
||||
"These are all the runs.": "已显示全部运行。",
|
||||
"Running…": "运行中…",
|
||||
"A cancelled session can start again on the next heartbeat; block the task to keep it stopped.": "取消的会话可能在下一次心跳时再次开始;如需保持停止,请阻塞该任务。",
|
||||
"A new session on the task picks up from what earlier sessions did and where they stopped.": "任务的新会话会接着之前会话做过的工作和停下的位置继续。",
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import { RunRequestList } from "./runs/RunRequestList";
|
||||
import { useMemo, useState } from "react";
|
||||
import { useRunRequests } from "./runs/use-run-requests";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { Avatar, Button, Pill, Segmented } from "../shared/ui";
|
||||
import type { AgentRecord, ProviderRecord, TaskRecord, TaskRunRecord } from "../../../shared/domain";
|
||||
import type { AgentRecord, ProviderRecord, RunCursor, TaskRecord, TaskRunRecord } from "../../../shared/domain";
|
||||
import type { PageProps } from "./types";
|
||||
import { activeOrgId, formatRelative } from "./types";
|
||||
import { t } from "../i18n/i18n";
|
||||
@@ -17,11 +18,48 @@ const FILTERS: Array<{ id: StatusFilter; label: string }> = [
|
||||
|
||||
export function RunsPage({ snapshot, actions, navigate, focus }: PageProps) {
|
||||
const orgId = activeOrgId(snapshot);
|
||||
const taskRuns = useMemo(() => snapshot.taskRuns.filter((r) => r.orgId === orgId), [snapshot.taskRuns, orgId]);
|
||||
const recentRuns = useMemo(() => snapshot.taskRuns.filter((r) => r.orgId === orgId), [snapshot.taskRuns, orgId]);
|
||||
// Older runs page in from the database; their range, from a pinned position, refreshes with each reload.
|
||||
const [pinned, setPinned] = useState<RunCursor | null>(null);
|
||||
const [olderRuns, setOlderRuns] = useState<TaskRunRecord[]>([]);
|
||||
const [olderDone, setOlderDone] = useState(false);
|
||||
const [historyError, setHistoryError] = useState<string | null>(null);
|
||||
const anchorId = recentRuns.length > 0 ? recentRuns[0].id : null;
|
||||
useEffect(() => {
|
||||
if (!pinned) return;
|
||||
let cancelled = false;
|
||||
window.agentCompany.runHistory({ orgId, beforeRunId: anchorId, from: pinned }).then((result) => {
|
||||
if (cancelled) return;
|
||||
if (!result.ok) throw new Error(result.error.message);
|
||||
setOlderRuns(result.data.runs);
|
||||
setOlderDone(!result.data.hasMore);
|
||||
setHistoryError(null);
|
||||
}).catch((error: unknown) => {
|
||||
if (!cancelled) setHistoryError(error instanceof Error ? error.message : String(error));
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [orgId, anchorId, pinned, recentRuns]);
|
||||
const taskRuns = useMemo(() => [...olderRuns, ...recentRuns], [olderRuns, recentRuns]);
|
||||
const oldestShownId = taskRuns.length > 0 ? taskRuns[0].id : null;
|
||||
const loadOlder = useCallback(() => {
|
||||
window.agentCompany.runHistory({ orgId, beforeRunId: oldestShownId, limit: 100 }).then((result) => {
|
||||
if (!result.ok) throw new Error(result.error.message);
|
||||
setHistoryError(null);
|
||||
// With nothing older, the oldest shown run's position is pinned so later reloads keep the range.
|
||||
const position = result.data.oldest ?? result.data.boundary;
|
||||
if (position) setPinned(position);
|
||||
setOlderDone(!result.data.hasMore);
|
||||
}).catch((error: unknown) => setHistoryError(error instanceof Error ? error.message : String(error)));
|
||||
}, [orgId, oldestShownId]);
|
||||
// While the snapshot holds none of this company's runs and nothing is pinned, each reload asks the database for the newest page.
|
||||
useEffect(() => {
|
||||
if (recentRuns.length === 0 && pinned === null) loadOlder();
|
||||
}, [recentRuns, pinned, loadOlder]);
|
||||
const tasks = useMemo(() => snapshot.tasks.filter((t) => t.orgId === orgId), [snapshot.tasks, orgId]);
|
||||
const agents = useMemo(() => snapshot.agents.filter((a) => a.orgId === orgId), [snapshot.agents, orgId]);
|
||||
const providers = useMemo(() => snapshot.providers.filter((p) => p.orgId === orgId), [snapshot.providers, orgId]);
|
||||
const providerRequests = useMemo(() => snapshot.providerRequests.filter((r) => r.orgId === orgId), [snapshot.providerRequests, orgId]);
|
||||
const [filter, setFilter] = useState<StatusFilter>("all");
|
||||
const [selectedId, setSelectedId] = useState<string | null>(focus?.kind === "run" ? focus.id : null);
|
||||
const [search, setSearch] = useState("");
|
||||
@@ -62,7 +100,8 @@ export function RunsPage({ snapshot, actions, navigate, focus }: PageProps) {
|
||||
<Segmented value={filter} onChange={setFilter} options={FILTERS} />
|
||||
</div>
|
||||
<div className="pane-scroll">
|
||||
{filtered.map((run) => {
|
||||
{/* Newest first, so "show older runs" continues below. */}
|
||||
{[...filtered].reverse().map((run) => {
|
||||
const task = tasks.find((t) => t.id === run.taskId);
|
||||
const agent = agents.find((a) => a.id === run.agentId);
|
||||
const active = run.id === selectedId;
|
||||
@@ -86,11 +125,17 @@ export function RunsPage({ snapshot, actions, navigate, focus }: PageProps) {
|
||||
);
|
||||
})}
|
||||
{filtered.length === 0 && <div style={{ padding: 16, fontSize: 14, color: "var(--text-3)" }}>{t("No runs")}</div>}
|
||||
{historyError && <div className="form-error" role="alert" style={{ padding: "0 16px" }}>{historyError}</div>}
|
||||
<div style={{ padding: 12, display: "flex", justifyContent: "center" }}>
|
||||
{olderDone
|
||||
? <span style={{ fontSize: 12, color: "var(--text-3)" }}>{t("These are all the runs.")}</span>
|
||||
: <Button size="small" onClick={loadOlder}>{t("Show older runs")}</Button>}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<div style={{ flex: 1, minWidth: 0, overflow: "auto", padding: 24 }}>
|
||||
{selected ? (
|
||||
<RunDetail key={selected.id} run={selected} tasks={tasks} agents={agents} providers={providers} providerRequests={providerRequests} actions={actions} onOpenTask={(taskId) => navigate("work", { kind: "task", id: taskId })} />
|
||||
<RunDetail key={selected.id} run={selected} tasks={tasks} agents={agents} providers={providers} actions={actions} onOpenTask={(taskId) => navigate("work", { kind: "task", id: taskId })} />
|
||||
) : (
|
||||
<div style={{ display: "grid", placeItems: "center", height: "100%", color: "var(--text-3)", fontSize: 14 }}>
|
||||
{t("Select a run")}
|
||||
@@ -101,19 +146,22 @@ export function RunsPage({ snapshot, actions, navigate, focus }: PageProps) {
|
||||
);
|
||||
}
|
||||
|
||||
function RunDetail({ run, tasks, agents, providers, providerRequests, actions, onOpenTask }: {
|
||||
function RunDetail({ run, tasks, agents, providers, actions, onOpenTask }: {
|
||||
actions: PageProps["actions"];
|
||||
onOpenTask: (taskId: string) => void;
|
||||
run: TaskRunRecord;
|
||||
tasks: TaskRecord[];
|
||||
agents: AgentRecord[];
|
||||
providers: ProviderRecord[];
|
||||
providerRequests: Array<{ id: string; orgId: string; providerId: string; model: string; state: string; responseText: string | null; inputCharCount: number; outputCharCount: number; createdAt: string; inputTokens: number | null; outputTokens: number | null; costCents: number | null }>;
|
||||
}) {
|
||||
const task = tasks.find((t) => t.id === run.taskId);
|
||||
const agent = agents.find((a) => a.id === run.agentId);
|
||||
const provider = providers.find((p) => p.id === run.providerId);
|
||||
const request = run.providerRequestId ? providerRequests.find((r) => r.id === run.providerRequestId) : null;
|
||||
// The run's calls come from the database, so an old run still shows its output; refetched with each snapshot reload.
|
||||
const loadedRequests = useRunRequests(run.orgId, run.id, run);
|
||||
const request = loadedRequests && "requests" in loadedRequests && run.providerRequestId
|
||||
? loadedRequests.requests.find((item) => item.id === run.providerRequestId) ?? null
|
||||
: null;
|
||||
|
||||
const started = run.startedAt ? new Date(run.startedAt) : null;
|
||||
const completed = run.completedAt ? new Date(run.completedAt) : null;
|
||||
@@ -165,7 +213,7 @@ function RunDetail({ run, tasks, agents, providers, providerRequests, actions, o
|
||||
<MetaLabel>{t("Context")}</MetaLabel><div className="mono" style={{ fontSize: 12, overflow: "hidden", textOverflow: "ellipsis" }}>{run.executionContextId}</div>
|
||||
</div>
|
||||
|
||||
<RunRequestList orgId={run.orgId} runId={run.id} state={run.state} />
|
||||
<RunRequestList loaded={loadedRequests} />
|
||||
|
||||
{run.state === "superseded" && run.errorMessage && (
|
||||
<div className="card" style={{ padding: 16, marginBottom: 16 }}>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { RunRequestRecord } from "../../../../shared/provider-runtime-domain";
|
||||
import type { LoadedRequests } from "./use-run-requests";
|
||||
import { Pill } from "../../shared/ui";
|
||||
import { t } from "../../i18n/i18n";
|
||||
|
||||
@@ -12,25 +11,8 @@ function purposeLabel(purpose: string): string {
|
||||
}
|
||||
|
||||
/** Every model call the run made, in order, so a turn or an audit can be found by its task. */
|
||||
export function RunRequestList({ orgId, runId, state }: { orgId: string; runId: string; state: string }) {
|
||||
// Fetched calls are kept with the run they belong to, so switching runs never shows another run's chain.
|
||||
const [loaded, setLoaded] = useState<{ runId: string; requests: RunRequestRecord[] } | { runId: string; error: string } | null>(null);
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
window.agentCompany.runRequests({ orgId, runId })
|
||||
.then((result) => {
|
||||
if (cancelled) return;
|
||||
setLoaded(result.ok ? { runId, requests: result.data } : { runId, error: result.error.message });
|
||||
})
|
||||
.catch((reason: unknown) => {
|
||||
if (!cancelled) setLoaded({ runId, error: reason instanceof Error ? reason.message : String(reason) });
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [orgId, runId, state]);
|
||||
|
||||
if (!loaded || loaded.runId !== runId) return null;
|
||||
export function RunRequestList({ loaded }: { loaded: LoadedRequests | null }) {
|
||||
if (!loaded) return null;
|
||||
if ("error" in loaded) return <div className="form-error" role="alert">{loaded.error}</div>;
|
||||
const { requests } = loaded;
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { RunRequestRecord } from "../../../../shared/provider-runtime-domain";
|
||||
|
||||
export type LoadedRequests = { runId: string; requests: RunRequestRecord[] } | { runId: string; error: string };
|
||||
|
||||
/** Every model call of one run, read from the database (not the snapshot's window), refetched whenever `refreshKey` changes. */
|
||||
export function useRunRequests(orgId: string, runId: string, refreshKey: unknown): LoadedRequests | null {
|
||||
// Fetched calls are kept with the run they belong to, so switching runs never shows another run's chain.
|
||||
const [loaded, setLoaded] = useState<LoadedRequests | null>(null);
|
||||
useEffect(() => {
|
||||
let cancelled = false;
|
||||
window.agentCompany.runRequests({ orgId, runId })
|
||||
.then((result) => {
|
||||
if (cancelled) return;
|
||||
setLoaded(result.ok ? { runId, requests: result.data } : { runId, error: result.error.message });
|
||||
})
|
||||
.catch((reason: unknown) => {
|
||||
if (!cancelled) setLoaded({ runId, error: reason instanceof Error ? reason.message : String(reason) });
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [orgId, runId, refreshKey]);
|
||||
return loaded && loaded.runId === runId ? loaded : null;
|
||||
}
|
||||
Reference in new issue
Block a user