@sentry/junior-dashboard 0.244.0 → 0.245.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/app.js +64 -7
- package/dist/client.js +33 -33
- package/dist/index.js +64 -7
- package/dist/tailwind.css +1 -1
- package/package.json +3 -3
- package/src/client/App.tsx +3 -2
- package/src/client/components/SegmentedTabs.tsx +11 -9
- package/src/client/conversations/ConversationAgentTree.tsx +42 -0
- package/src/client/conversations/ConversationChildAgents.tsx +134 -58
- package/src/client/conversations/ConversationDetailsDrawer.tsx +29 -9
- package/src/client/conversations/ConversationHeader.tsx +13 -1
- package/src/client/conversations/ConversationMemories.tsx +10 -5
- package/src/client/conversations/ConversationPage.tsx +16 -3
- package/src/client/conversations/SubagentTranscriptDrawer.tsx +30 -23
- package/src/client/format.ts +15 -0
- package/src/client/pages/dev/SegmentedFixtures.tsx +1 -1
- package/src/mock-reporting/fixtures.ts +68 -2
|
@@ -19,11 +19,12 @@ export function segmentedTabClass(selected: boolean): string {
|
|
|
19
19
|
);
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
/** Switch between related panels
|
|
22
|
+
/** Switch between related panels. Tabs share the row equally on wide screens. */
|
|
23
23
|
export function SegmentedTabs<const Value extends string>(props: {
|
|
24
24
|
children: ReactNode;
|
|
25
25
|
label: string;
|
|
26
|
-
|
|
26
|
+
/** `count` is the number of entries in the panel. Zero and unknown counts are hidden. */
|
|
27
|
+
items: readonly { count?: number; label: string; value: Value }[];
|
|
27
28
|
onChange(value: NoInfer<Value>): void;
|
|
28
29
|
value: NoInfer<Value>;
|
|
29
30
|
}) {
|
|
@@ -33,11 +34,7 @@ export function SegmentedTabs<const Value extends string>(props: {
|
|
|
33
34
|
<div className="grid min-w-0 gap-4">
|
|
34
35
|
<div
|
|
35
36
|
aria-label={props.label}
|
|
36
|
-
className={cn(
|
|
37
|
-
"grid auto-cols-fr grid-flow-col",
|
|
38
|
-
segmentedTabsFrameClass,
|
|
39
|
-
segmentedTabsInsetClass,
|
|
40
|
-
)}
|
|
37
|
+
className={cn("flex", segmentedTabsFrameClass, segmentedTabsInsetClass)}
|
|
41
38
|
role="tablist"
|
|
42
39
|
>
|
|
43
40
|
{props.items.map((item, index) => (
|
|
@@ -45,7 +42,7 @@ export function SegmentedTabs<const Value extends string>(props: {
|
|
|
45
42
|
aria-controls={`${id}-panel`}
|
|
46
43
|
aria-selected={props.value === item.value}
|
|
47
44
|
className={cn(
|
|
48
|
-
"min-w-0",
|
|
45
|
+
"inline-flex min-w-0 flex-auto items-center justify-center gap-1.5 sm:flex-1",
|
|
49
46
|
segmentedTabClass(props.value === item.value),
|
|
50
47
|
)}
|
|
51
48
|
id={`${id}-${item.value}`}
|
|
@@ -80,7 +77,12 @@ export function SegmentedTabs<const Value extends string>(props: {
|
|
|
80
77
|
tabIndex={props.value === item.value ? 0 : -1}
|
|
81
78
|
type="button"
|
|
82
79
|
>
|
|
83
|
-
{item.label}
|
|
80
|
+
<span className="truncate">{item.label}</span>
|
|
81
|
+
{item.count ? (
|
|
82
|
+
<span className="min-w-5 shrink-0 rounded-full bg-dashboard-fill-strong px-1.5 text-center text-xs font-semibold leading-5 tabular-nums">
|
|
83
|
+
{item.count.toLocaleString("en-US")}
|
|
84
|
+
</span>
|
|
85
|
+
) : null}
|
|
84
86
|
</button>
|
|
85
87
|
))}
|
|
86
88
|
</div>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Bot, ChevronRight } from "lucide-react";
|
|
2
|
+
import { Link } from "react-router";
|
|
3
|
+
import type { ConversationAgentTreeNode } from "@sentry/junior/api/schema";
|
|
4
|
+
|
|
5
|
+
import { conversationPath } from "./conversationRoutes";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Show the parents of a child Conversation, root first. The page title already
|
|
9
|
+
* names this agent, so the last node is not shown. Root Conversations have no
|
|
10
|
+
* tree.
|
|
11
|
+
*/
|
|
12
|
+
export function ConversationAgentTree(props: {
|
|
13
|
+
nodes: readonly ConversationAgentTreeNode[];
|
|
14
|
+
}) {
|
|
15
|
+
return (
|
|
16
|
+
<nav
|
|
17
|
+
aria-label="Agent tree"
|
|
18
|
+
className="flex min-w-0 items-center gap-1.5 text-xs leading-snug text-dashboard-text-muted"
|
|
19
|
+
>
|
|
20
|
+
<Bot aria-hidden="true" className="shrink-0 text-cyan-300" size={12} />
|
|
21
|
+
<ol className="m-0 flex min-w-0 list-none items-center gap-1 p-0">
|
|
22
|
+
{props.nodes.slice(0, -1).map((node, index) => (
|
|
23
|
+
<li
|
|
24
|
+
className="inline-flex min-w-0 items-center gap-1"
|
|
25
|
+
key={node.conversationId}
|
|
26
|
+
>
|
|
27
|
+
{index > 0 ? (
|
|
28
|
+
<ChevronRight aria-hidden="true" className="shrink-0" size={12} />
|
|
29
|
+
) : null}
|
|
30
|
+
<Link
|
|
31
|
+
className="min-w-0 max-w-[16rem] truncate font-medium text-dashboard-text no-underline underline-offset-2 hover:underline"
|
|
32
|
+
title={node.label}
|
|
33
|
+
to={conversationPath(node.conversationId)}
|
|
34
|
+
>
|
|
35
|
+
{node.label}
|
|
36
|
+
</Link>
|
|
37
|
+
</li>
|
|
38
|
+
))}
|
|
39
|
+
</ol>
|
|
40
|
+
</nav>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -1,42 +1,65 @@
|
|
|
1
1
|
import { useQuery } from "@tanstack/react-query";
|
|
2
|
-
import { Bot } from "lucide-react";
|
|
3
|
-
import { Link } from "react-router";
|
|
2
|
+
import { Bot, CircleAlert, LockKeyhole } from "lucide-react";
|
|
4
3
|
import {
|
|
5
4
|
conversationChildAgentsReportSchema,
|
|
6
5
|
type ConversationChildAgent,
|
|
7
6
|
} from "@sentry/junior/api/schema";
|
|
8
7
|
|
|
9
8
|
import { StatusChip, type StatusChipTone } from "../components/StatusChip";
|
|
10
|
-
import {
|
|
9
|
+
import {
|
|
10
|
+
formatConversationCostTotal,
|
|
11
|
+
formatRelativeTime,
|
|
12
|
+
formatRuntime,
|
|
13
|
+
formatTime,
|
|
14
|
+
formatUsageTotal,
|
|
15
|
+
} from "../format";
|
|
11
16
|
import { fetchDashboardJson } from "../http";
|
|
12
|
-
import {
|
|
17
|
+
import { cn } from "../styles";
|
|
18
|
+
import type { SubagentTranscriptTarget } from "./SubagentTranscriptDrawer";
|
|
13
19
|
|
|
14
|
-
const
|
|
20
|
+
const statusTones: Partial<Record<string, StatusChipTone>> = {
|
|
21
|
+
active: "info",
|
|
15
22
|
blocked: "warning",
|
|
16
23
|
completed: "success",
|
|
24
|
+
error: "danger",
|
|
17
25
|
failed: "danger",
|
|
18
26
|
paused: "info",
|
|
19
|
-
pending: "neutral",
|
|
20
27
|
running: "info",
|
|
21
|
-
stopped: "neutral",
|
|
22
28
|
};
|
|
23
29
|
|
|
24
|
-
/**
|
|
25
|
-
export function
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
30
|
+
/** Pick the status tone for a child agent, its transcript row, or its detail. */
|
|
31
|
+
export function subagentStatusTone(status: string): StatusChipTone {
|
|
32
|
+
return statusTones[status] ?? "neutral";
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const errorToneClass: Partial<Record<StatusChipTone, string>> = {
|
|
36
|
+
danger: "text-rose-200/85",
|
|
37
|
+
warning: "text-amber-200/85",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** Load the child agents of one Conversation with runtime status. */
|
|
41
|
+
export function useConversationChildAgents(conversationId: string) {
|
|
42
|
+
return useQuery({
|
|
31
43
|
queryFn: ({ signal }) =>
|
|
32
44
|
fetchDashboardJson(
|
|
33
45
|
conversationChildAgentsReportSchema,
|
|
34
|
-
`/api/conversations/${encodeURIComponent(
|
|
46
|
+
`/api/conversations/${encodeURIComponent(conversationId)}/agents`,
|
|
35
47
|
signal,
|
|
36
48
|
),
|
|
37
|
-
queryKey: ["conversation",
|
|
49
|
+
queryKey: ["conversation", conversationId, "agents"],
|
|
38
50
|
retry: false,
|
|
39
51
|
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Show the child agents of one Conversation with runtime status. A row opens
|
|
56
|
+
* that child's transcript.
|
|
57
|
+
*/
|
|
58
|
+
export function ConversationChildAgents(props: {
|
|
59
|
+
conversationId: string;
|
|
60
|
+
onOpenAgent(target: SubagentTranscriptTarget): void;
|
|
61
|
+
}) {
|
|
62
|
+
const query = useConversationChildAgents(props.conversationId);
|
|
40
63
|
|
|
41
64
|
if (query.isPending) {
|
|
42
65
|
return (
|
|
@@ -62,51 +85,104 @@ export function ConversationChildAgents(props: {
|
|
|
62
85
|
}
|
|
63
86
|
|
|
64
87
|
return (
|
|
65
|
-
<ol aria-label="Subagents" className="m-0 grid list-none gap-
|
|
88
|
+
<ol aria-label="Subagents" className="m-0 grid list-none gap-2 p-0">
|
|
66
89
|
{query.data.agents.map((agent) => (
|
|
67
|
-
<li
|
|
68
|
-
|
|
69
|
-
key={agent.conversationId}
|
|
70
|
-
>
|
|
71
|
-
<div className="flex min-w-0 flex-wrap items-center gap-2">
|
|
72
|
-
<Link
|
|
73
|
-
className="min-w-0 truncate font-mono text-xs font-semibold text-dashboard-text no-underline hover:underline focus-visible:outline focus-visible:outline-2 focus-visible:outline-dashboard-focus"
|
|
74
|
-
onClick={props.onNavigate}
|
|
75
|
-
to={conversationPath(agent.conversationId)}
|
|
76
|
-
>
|
|
77
|
-
{agent.redacted ? "Private subagent" : (agent.name ?? "agent")}
|
|
78
|
-
</Link>
|
|
79
|
-
<StatusChip
|
|
80
|
-
className="ml-auto"
|
|
81
|
-
size="compact"
|
|
82
|
-
tone={statusTone[agent.status]}
|
|
83
|
-
>
|
|
84
|
-
{agent.status}
|
|
85
|
-
</StatusChip>
|
|
86
|
-
</div>
|
|
87
|
-
{agent.title ? (
|
|
88
|
-
<p className="m-0 break-words text-sm font-semibold text-dashboard-text">
|
|
89
|
-
{agent.title}
|
|
90
|
-
</p>
|
|
91
|
-
) : null}
|
|
92
|
-
{agent.task ? (
|
|
93
|
-
<p className="m-0 line-clamp-4 whitespace-pre-wrap break-words text-sm leading-relaxed text-dashboard-text">
|
|
94
|
-
{agent.task}
|
|
95
|
-
</p>
|
|
96
|
-
) : null}
|
|
97
|
-
{agent.error ? (
|
|
98
|
-
<p className="m-0 whitespace-pre-wrap break-words text-xs leading-relaxed text-dashboard-text-muted">
|
|
99
|
-
{agent.error}
|
|
100
|
-
</p>
|
|
101
|
-
) : null}
|
|
102
|
-
<time
|
|
103
|
-
className="font-mono text-xs text-dashboard-text-muted"
|
|
104
|
-
dateTime={agent.updatedAt}
|
|
105
|
-
>
|
|
106
|
-
{formatMessageTimestamp(Date.parse(agent.updatedAt), true)}
|
|
107
|
-
</time>
|
|
90
|
+
<li key={agent.conversationId}>
|
|
91
|
+
<ChildAgentRow agent={agent} onOpen={props.onOpenAgent} />
|
|
108
92
|
</li>
|
|
109
93
|
))}
|
|
110
94
|
</ol>
|
|
111
95
|
);
|
|
112
96
|
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Show who ran, what it was asked to do, and what it cost. The row text is the
|
|
100
|
+
* accessible name, so status, title, task, and error stay audible.
|
|
101
|
+
*/
|
|
102
|
+
function ChildAgentRow(props: {
|
|
103
|
+
agent: ConversationChildAgent;
|
|
104
|
+
onOpen(target: SubagentTranscriptTarget): void;
|
|
105
|
+
}) {
|
|
106
|
+
const { agent } = props;
|
|
107
|
+
const tone = subagentStatusTone(agent.status);
|
|
108
|
+
const task = agent.task?.trim();
|
|
109
|
+
const headline = agent.title ?? task;
|
|
110
|
+
// A title is often a short form of the task. Skip the task when it adds nothing.
|
|
111
|
+
const detail = agent.title && task && task !== agent.title ? task : undefined;
|
|
112
|
+
const runtime = formatRuntime(agent.cumulativeDurationMs);
|
|
113
|
+
const stats = [
|
|
114
|
+
formatUsageTotal(agent.cumulativeUsage),
|
|
115
|
+
formatConversationCostTotal(agent.cumulativeUsage, agent.auxiliaryCosts),
|
|
116
|
+
runtime ? `${runtime} runtime` : "",
|
|
117
|
+
].filter(Boolean);
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<button
|
|
121
|
+
className="grid w-full min-w-0 cursor-pointer gap-1.5 rounded-lg border border-dashboard-border-subtle bg-dashboard-surface px-3 py-2.5 text-left text-inherit transition-colors duration-200 hover:border-dashboard-border-emphasis hover:bg-dashboard-surface-hover focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-dashboard-focus"
|
|
122
|
+
onClick={() =>
|
|
123
|
+
props.onOpen({
|
|
124
|
+
conversationId: agent.conversationId,
|
|
125
|
+
...(agent.redacted
|
|
126
|
+
? undefined
|
|
127
|
+
: { label: agent.title ?? agent.name }),
|
|
128
|
+
status: agent.status,
|
|
129
|
+
})
|
|
130
|
+
}
|
|
131
|
+
type="button"
|
|
132
|
+
>
|
|
133
|
+
<span className="flex min-w-0 items-center gap-2 text-xs">
|
|
134
|
+
{agent.redacted ? (
|
|
135
|
+
<span className="flex min-w-0 items-center gap-1.5 text-dashboard-text-muted">
|
|
136
|
+
<LockKeyhole aria-hidden="true" className="size-3 shrink-0" />
|
|
137
|
+
Private subagent
|
|
138
|
+
</span>
|
|
139
|
+
) : (
|
|
140
|
+
<span className="min-w-0 truncate font-mono text-dashboard-text-muted">
|
|
141
|
+
{agent.name ?? "subagent"}
|
|
142
|
+
</span>
|
|
143
|
+
)}
|
|
144
|
+
<StatusChip className="ml-auto shrink-0" size="compact" tone={tone}>
|
|
145
|
+
{agent.status}
|
|
146
|
+
</StatusChip>
|
|
147
|
+
<time
|
|
148
|
+
className="shrink-0 text-dashboard-text-faint"
|
|
149
|
+
dateTime={agent.updatedAt}
|
|
150
|
+
title={`Updated ${formatTime(agent.updatedAt, {
|
|
151
|
+
dateStyle: "medium",
|
|
152
|
+
timeStyle: "short",
|
|
153
|
+
})}`}
|
|
154
|
+
>
|
|
155
|
+
{formatRelativeTime(agent.updatedAt)}
|
|
156
|
+
</time>
|
|
157
|
+
</span>
|
|
158
|
+
{headline ? (
|
|
159
|
+
<span className="line-clamp-2 break-words text-sm font-semibold leading-snug text-dashboard-text">
|
|
160
|
+
{headline}
|
|
161
|
+
</span>
|
|
162
|
+
) : null}
|
|
163
|
+
{detail ? (
|
|
164
|
+
<span className="line-clamp-2 whitespace-pre-wrap break-words text-sm leading-relaxed text-dashboard-text-muted">
|
|
165
|
+
{detail}
|
|
166
|
+
</span>
|
|
167
|
+
) : null}
|
|
168
|
+
{agent.error ? (
|
|
169
|
+
<span
|
|
170
|
+
className={cn(
|
|
171
|
+
"flex min-w-0 items-start gap-1.5 text-xs leading-relaxed",
|
|
172
|
+
errorToneClass[tone] ?? "text-dashboard-text-muted",
|
|
173
|
+
)}
|
|
174
|
+
>
|
|
175
|
+
<CircleAlert aria-hidden="true" className="mt-0.5 size-3 shrink-0" />
|
|
176
|
+
<span className="line-clamp-3 min-w-0 whitespace-pre-wrap break-words">
|
|
177
|
+
{agent.error}
|
|
178
|
+
</span>
|
|
179
|
+
</span>
|
|
180
|
+
) : null}
|
|
181
|
+
{stats.length > 0 ? (
|
|
182
|
+
<span className="block font-mono text-xs tabular-nums text-dashboard-text-muted">
|
|
183
|
+
{stats.join(" · ")}
|
|
184
|
+
</span>
|
|
185
|
+
) : null}
|
|
186
|
+
</button>
|
|
187
|
+
);
|
|
188
|
+
}
|
|
@@ -5,15 +5,18 @@ import { formatRelativeTime, formatTime } from "../format";
|
|
|
5
5
|
import { ButtonLink } from "../components/Button";
|
|
6
6
|
import { Drawer } from "../components/Drawer";
|
|
7
7
|
import { SegmentedTabs } from "../components/SegmentedTabs";
|
|
8
|
-
import {
|
|
9
|
-
|
|
8
|
+
import {
|
|
9
|
+
ConversationChildAgents,
|
|
10
|
+
useConversationChildAgents,
|
|
11
|
+
} from "./ConversationChildAgents";
|
|
12
|
+
import {
|
|
13
|
+
ConversationMemories,
|
|
14
|
+
useConversationMemories,
|
|
15
|
+
} from "./ConversationMemories";
|
|
10
16
|
import { ShareConversationButton } from "./ShareConversationButton";
|
|
17
|
+
import type { SubagentTranscriptTarget } from "./SubagentTranscriptDrawer";
|
|
11
18
|
|
|
12
|
-
|
|
13
|
-
{ label: "Details", value: "details" },
|
|
14
|
-
{ label: "Memories", value: "memories" },
|
|
15
|
-
{ label: "Subagents", value: "agents" },
|
|
16
|
-
] as const;
|
|
19
|
+
type DetailsTab = "agents" | "details" | "memories";
|
|
17
20
|
|
|
18
21
|
/** Put conversation context before usage and diagnostic links. */
|
|
19
22
|
export function ConversationDetailsDrawer(props: {
|
|
@@ -23,12 +26,26 @@ export function ConversationDetailsDrawer(props: {
|
|
|
23
26
|
identity: ReactNode;
|
|
24
27
|
lastActivityAt?: string;
|
|
25
28
|
onClose(): void;
|
|
29
|
+
/** Open one child agent transcript. The details drawer closes first. */
|
|
30
|
+
onOpenSubagent(target: SubagentTranscriptTarget): void;
|
|
26
31
|
sentryConversationUrl?: string;
|
|
27
32
|
privacy: ReactNode;
|
|
28
33
|
stats: ReactNode;
|
|
29
34
|
title: string;
|
|
30
35
|
}) {
|
|
31
|
-
const [tab, setTab] = useState<
|
|
36
|
+
const [tab, setTab] = useState<DetailsTab>("details");
|
|
37
|
+
// Load both lists with the drawer so each tab can show its count.
|
|
38
|
+
const memories = useConversationMemories(props.conversationId);
|
|
39
|
+
const agents = useConversationChildAgents(props.conversationId);
|
|
40
|
+
const tabs = [
|
|
41
|
+
{ label: "Details", value: "details" },
|
|
42
|
+
{
|
|
43
|
+
count: memories.data?.memories.length,
|
|
44
|
+
label: "Memories",
|
|
45
|
+
value: "memories",
|
|
46
|
+
},
|
|
47
|
+
{ count: agents.data?.agents.length, label: "Subagents", value: "agents" },
|
|
48
|
+
] as const;
|
|
32
49
|
const titleId = "conversation-details-drawer-title";
|
|
33
50
|
const sections = [
|
|
34
51
|
{ content: props.brief, title: "Summary" },
|
|
@@ -70,7 +87,10 @@ export function ConversationDetailsDrawer(props: {
|
|
|
70
87
|
) : tab === "agents" ? (
|
|
71
88
|
<ConversationChildAgents
|
|
72
89
|
conversationId={props.conversationId}
|
|
73
|
-
|
|
90
|
+
onOpenAgent={(target) => {
|
|
91
|
+
props.onClose();
|
|
92
|
+
props.onOpenSubagent(target);
|
|
93
|
+
}}
|
|
74
94
|
/>
|
|
75
95
|
) : sections.length > 0 ? (
|
|
76
96
|
<div className="grid min-w-0 gap-5">
|
|
@@ -24,9 +24,12 @@ import {
|
|
|
24
24
|
} from "./ConversationHeaderActions";
|
|
25
25
|
import { ConversationDetailsDrawer } from "./ConversationDetailsDrawer";
|
|
26
26
|
import type { TranscriptViewMode } from "./transcriptRenderModel";
|
|
27
|
+
import type { SubagentTranscriptTarget } from "./SubagentTranscriptDrawer";
|
|
27
28
|
|
|
28
29
|
/** Show the conversation title, search, and details. */
|
|
29
30
|
export function ConversationHeader(props: {
|
|
31
|
+
/** Parent chain of a child Conversation. Root Conversations omit it. */
|
|
32
|
+
agentTree?: ReactNode;
|
|
30
33
|
copyAction?: ReactNode;
|
|
31
34
|
annotations: ReactNode;
|
|
32
35
|
archive: ConversationArchiveAction;
|
|
@@ -40,6 +43,7 @@ export function ConversationHeader(props: {
|
|
|
40
43
|
sentryConversationUrl?: string;
|
|
41
44
|
live: boolean;
|
|
42
45
|
meta?: ReactNode;
|
|
46
|
+
onOpenSubagent(target: SubagentTranscriptTarget): void;
|
|
43
47
|
onSearchChange(value: string): void;
|
|
44
48
|
onViewChange(value: TranscriptViewMode): void;
|
|
45
49
|
privacy: ReactNode;
|
|
@@ -154,7 +158,8 @@ export function ConversationHeader(props: {
|
|
|
154
158
|
searchOpenVisible ||
|
|
155
159
|
props.archive.error ||
|
|
156
160
|
Boolean(props.linkedWork) ||
|
|
157
|
-
Boolean(props.forks)
|
|
161
|
+
Boolean(props.forks) ||
|
|
162
|
+
Boolean(props.agentTree);
|
|
158
163
|
|
|
159
164
|
const liveIndicator = props.live ? (
|
|
160
165
|
<span
|
|
@@ -176,6 +181,9 @@ export function ConversationHeader(props: {
|
|
|
176
181
|
>
|
|
177
182
|
<div className="hidden min-w-0 items-start justify-between gap-4 px-7 py-5 md:flex">
|
|
178
183
|
<div className="min-w-0">
|
|
184
|
+
{props.agentTree ? (
|
|
185
|
+
<div className="mb-1.5 min-w-0">{props.agentTree}</div>
|
|
186
|
+
) : null}
|
|
179
187
|
<div className="flex min-w-0 items-center gap-2">
|
|
180
188
|
<h2 className="m-0 line-clamp-1 min-w-0 font-display text-xl font-medium leading-tight tracking-tight">
|
|
181
189
|
{props.title}
|
|
@@ -204,6 +212,9 @@ export function ConversationHeader(props: {
|
|
|
204
212
|
view={props.view}
|
|
205
213
|
/>
|
|
206
214
|
</div>
|
|
215
|
+
{props.agentTree ? (
|
|
216
|
+
<div className="px-4 py-2 md:hidden">{props.agentTree}</div>
|
|
217
|
+
) : null}
|
|
207
218
|
{props.forks ? (
|
|
208
219
|
<div className="px-4 py-2 md:hidden">{props.forks}</div>
|
|
209
220
|
) : null}
|
|
@@ -251,6 +262,7 @@ export function ConversationHeader(props: {
|
|
|
251
262
|
lastActivityAt={props.lastActivityAt}
|
|
252
263
|
sentryConversationUrl={props.sentryConversationUrl}
|
|
253
264
|
onClose={() => setDetailsOpen(false)}
|
|
265
|
+
onOpenSubagent={props.onOpenSubagent}
|
|
254
266
|
privacy={props.privacy}
|
|
255
267
|
stats={props.stats}
|
|
256
268
|
title={props.title}
|
|
@@ -21,18 +21,23 @@ const conversationMemoryListSchema = z
|
|
|
21
21
|
})
|
|
22
22
|
.strict();
|
|
23
23
|
|
|
24
|
-
/** Load
|
|
25
|
-
export function
|
|
26
|
-
|
|
24
|
+
/** Load the memories captured from one Conversation. */
|
|
25
|
+
export function useConversationMemories(conversationId: string) {
|
|
26
|
+
return useQuery({
|
|
27
27
|
queryFn: ({ signal }) =>
|
|
28
28
|
fetchDashboardJson(
|
|
29
29
|
conversationMemoryListSchema,
|
|
30
|
-
`/api/plugins/memory/conversations/${encodeURIComponent(
|
|
30
|
+
`/api/plugins/memory/conversations/${encodeURIComponent(conversationId)}/memories`,
|
|
31
31
|
signal,
|
|
32
32
|
),
|
|
33
|
-
queryKey: ["conversation",
|
|
33
|
+
queryKey: ["conversation", conversationId, "memories"],
|
|
34
34
|
retry: false,
|
|
35
35
|
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Show the memories captured from one Conversation. */
|
|
39
|
+
export function ConversationMemories(props: { conversationId: string }) {
|
|
40
|
+
const query = useConversationMemories(props.conversationId);
|
|
36
41
|
|
|
37
42
|
if (query.isPending) {
|
|
38
43
|
return (
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
import { ConversationComposer } from "./ConversationComposer";
|
|
27
27
|
import { ConversationHeader } from "./ConversationHeader";
|
|
28
28
|
import { ConversationForkLinks } from "./ConversationForkLinks";
|
|
29
|
+
import { ConversationAgentTree } from "./ConversationAgentTree";
|
|
29
30
|
import { ConversationHeaderMeta } from "./ConversationHeaderMeta";
|
|
30
31
|
import {
|
|
31
32
|
ConversationAnnotations,
|
|
@@ -38,6 +39,7 @@ import {
|
|
|
38
39
|
import { PendingMailboxStack } from "./PendingMailboxStack";
|
|
39
40
|
import {
|
|
40
41
|
buildConversations,
|
|
42
|
+
conversationDetailTitle,
|
|
41
43
|
conversationDisplayTitle,
|
|
42
44
|
conversationFromDetail,
|
|
43
45
|
visualStatusForConversation,
|
|
@@ -103,6 +105,7 @@ export function ConversationPage(props: {
|
|
|
103
105
|
<ParticipantAvatarStack participants={participants} size="detail" />
|
|
104
106
|
) : null;
|
|
105
107
|
const conversationDetail = detail.data;
|
|
108
|
+
const agentTree = detail.data?.agentTree;
|
|
106
109
|
useEffect(() => {
|
|
107
110
|
if (!conversation) return;
|
|
108
111
|
onRead?.(conversation.id, conversation.lastSeenAt);
|
|
@@ -177,7 +180,8 @@ export function ConversationPage(props: {
|
|
|
177
180
|
}) => {
|
|
178
181
|
setSubagentTarget({
|
|
179
182
|
conversationId: part.childConversationId,
|
|
180
|
-
part,
|
|
183
|
+
label: part.title ?? part.agentName,
|
|
184
|
+
status: part.status,
|
|
181
185
|
});
|
|
182
186
|
},
|
|
183
187
|
[],
|
|
@@ -192,6 +196,9 @@ export function ConversationPage(props: {
|
|
|
192
196
|
scroll={
|
|
193
197
|
<section className="min-w-0">
|
|
194
198
|
<ConversationHeader
|
|
199
|
+
agentTree={
|
|
200
|
+
agentTree ? <ConversationAgentTree nodes={agentTree} /> : null
|
|
201
|
+
}
|
|
195
202
|
conversationId={conversationId}
|
|
196
203
|
forks={
|
|
197
204
|
detail.data?.forkedFromConversationId ||
|
|
@@ -273,6 +280,7 @@ export function ConversationPage(props: {
|
|
|
273
280
|
}
|
|
274
281
|
/>
|
|
275
282
|
}
|
|
283
|
+
onOpenSubagent={setSubagentTarget}
|
|
276
284
|
onSearchChange={setSearch}
|
|
277
285
|
onViewChange={onViewChange}
|
|
278
286
|
privacy={
|
|
@@ -292,7 +300,10 @@ export function ConversationPage(props: {
|
|
|
292
300
|
/>
|
|
293
301
|
) : null
|
|
294
302
|
}
|
|
295
|
-
title={
|
|
303
|
+
title={
|
|
304
|
+
conversationDetailTitle(detail.data) ??
|
|
305
|
+
conversationDisplayTitle(conversation)
|
|
306
|
+
}
|
|
296
307
|
view={view}
|
|
297
308
|
/>
|
|
298
309
|
|
|
@@ -341,7 +352,9 @@ export function ConversationPage(props: {
|
|
|
341
352
|
</section>
|
|
342
353
|
}
|
|
343
354
|
dock={
|
|
344
|
-
|
|
355
|
+
// A child Conversation takes input only from its parent agent, and it
|
|
356
|
+
// runs without interactive auth, so it has no approval to show.
|
|
357
|
+
view === "rich" && detail.data?.isParticipant && !agentTree ? (
|
|
345
358
|
<ConversationReplyFooter
|
|
346
359
|
conversationId={conversationId}
|
|
347
360
|
// Only pass committed ids for mailbox de-dupe. The full transcript is
|
|
@@ -4,11 +4,12 @@ import { Link } from "react-router";
|
|
|
4
4
|
|
|
5
5
|
import { useConversationData } from "./queries";
|
|
6
6
|
import { conversationPath } from "./conversationRoutes";
|
|
7
|
-
import { formatMessageTimestamp } from "../format";
|
|
7
|
+
import { conversationDetailTitle, formatMessageTimestamp } from "../format";
|
|
8
8
|
import { buildConversationMarkdown } from "../markdownExport";
|
|
9
|
-
import type { TranscriptViewSubagentPart } from "../types";
|
|
10
9
|
import { Drawer } from "../components/Drawer";
|
|
11
10
|
import { SearchInput } from "../components/SearchInput";
|
|
11
|
+
import { StatusChip } from "../components/StatusChip";
|
|
12
|
+
import { subagentStatusTone } from "./ConversationChildAgents";
|
|
12
13
|
import { CopyMarkdownButton } from "./CopyMarkdownButton";
|
|
13
14
|
import {
|
|
14
15
|
TranscriptSearchToggle,
|
|
@@ -19,9 +20,13 @@ import { TranscriptLoading } from "./TranscriptLoading";
|
|
|
19
20
|
import type { TranscriptViewMode } from "./transcriptRenderModel";
|
|
20
21
|
import { transcriptEmptyClass } from "./transcriptStyles";
|
|
21
22
|
|
|
23
|
+
/** One child Conversation to show, with what its parent already knows. */
|
|
22
24
|
export interface SubagentTranscriptTarget {
|
|
23
25
|
conversationId: string;
|
|
24
|
-
|
|
26
|
+
/** Task title or agent name, shown before the child loads. */
|
|
27
|
+
label?: string;
|
|
28
|
+
/** Child status from the parent, which knows how the child ended. */
|
|
29
|
+
status?: string;
|
|
25
30
|
}
|
|
26
31
|
|
|
27
32
|
/** Show a child conversation through the ordinary authorized detail API. */
|
|
@@ -51,13 +56,8 @@ function SubagentTranscriptDrawerContent(props: {
|
|
|
51
56
|
|
|
52
57
|
const detail = query.data;
|
|
53
58
|
const label =
|
|
54
|
-
props.target.
|
|
55
|
-
|
|
56
|
-
props.target.part.subagentKind;
|
|
57
|
-
const meta = [
|
|
58
|
-
statusLabel(props.target.part, detail?.status),
|
|
59
|
-
detail ? formatMessageTimestamp(Date.parse(detail.startedAt)) : undefined,
|
|
60
|
-
].filter(isString);
|
|
59
|
+
props.target.label ?? conversationDetailTitle(detail) ?? "Subagent";
|
|
60
|
+
const status = statusLabel(props.target.status, detail?.status);
|
|
61
61
|
const searchOpenVisible = searchOpen || search.length > 0;
|
|
62
62
|
|
|
63
63
|
const titleId = "subagent-transcript-drawer-title";
|
|
@@ -122,9 +122,20 @@ function SubagentTranscriptDrawerContent(props: {
|
|
|
122
122
|
<ExternalLink aria-hidden="true" size={12} strokeWidth={2.25} />
|
|
123
123
|
</Link>
|
|
124
124
|
</div>
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
{status || detail ? (
|
|
126
|
+
<div className="mt-1 flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-xs leading-snug text-dashboard-text-muted">
|
|
127
|
+
{status ? (
|
|
128
|
+
<StatusChip size="compact" tone={subagentStatusTone(status)}>
|
|
129
|
+
{status}
|
|
130
|
+
</StatusChip>
|
|
131
|
+
) : null}
|
|
132
|
+
{detail ? (
|
|
133
|
+
<time className="font-mono" dateTime={detail.startedAt}>
|
|
134
|
+
{formatMessageTimestamp(Date.parse(detail.startedAt))}
|
|
135
|
+
</time>
|
|
136
|
+
) : null}
|
|
137
|
+
</div>
|
|
138
|
+
) : null}
|
|
128
139
|
{searchOpenVisible ? (
|
|
129
140
|
<div className="mt-3 min-w-0">
|
|
130
141
|
<SearchInput
|
|
@@ -169,13 +180,13 @@ function SubagentTranscriptDrawerContent(props: {
|
|
|
169
180
|
}
|
|
170
181
|
|
|
171
182
|
function statusLabel(
|
|
172
|
-
|
|
173
|
-
detailStatus
|
|
174
|
-
): string {
|
|
175
|
-
if (
|
|
176
|
-
return
|
|
183
|
+
status: string | undefined,
|
|
184
|
+
detailStatus: string | undefined,
|
|
185
|
+
): string | undefined {
|
|
186
|
+
if (status && status !== "running" && status !== "completed") {
|
|
187
|
+
return status;
|
|
177
188
|
}
|
|
178
|
-
return detailStatus ??
|
|
189
|
+
return detailStatus ?? status;
|
|
179
190
|
}
|
|
180
191
|
|
|
181
192
|
function DrawerEmptyState(props: {
|
|
@@ -193,7 +204,3 @@ function DrawerEmptyState(props: {
|
|
|
193
204
|
</div>
|
|
194
205
|
);
|
|
195
206
|
}
|
|
196
|
-
|
|
197
|
-
function isString(value: string | undefined): value is string {
|
|
198
|
-
return typeof value === "string" && value.length > 0;
|
|
199
|
-
}
|
package/src/client/format.ts
CHANGED
|
@@ -660,6 +660,21 @@ function compareTimeDesc(a: string | undefined, b: string | undefined): number {
|
|
|
660
660
|
return (parseTime(b) ?? 0) - (parseTime(a) ?? 0);
|
|
661
661
|
}
|
|
662
662
|
|
|
663
|
+
/**
|
|
664
|
+
* Return the title of a loaded Conversation. A child agent Conversation uses
|
|
665
|
+
* its agent label from the agent tree, because it has no title of its own.
|
|
666
|
+
*/
|
|
667
|
+
export function conversationDetailTitle(
|
|
668
|
+
detail:
|
|
669
|
+
| Pick<ConversationTranscript, "agentTree" | "displayTitle">
|
|
670
|
+
| undefined,
|
|
671
|
+
): string | undefined {
|
|
672
|
+
return (
|
|
673
|
+
detail?.agentTree?.at(-1)?.label ??
|
|
674
|
+
(detail?.displayTitle?.trim() || undefined)
|
|
675
|
+
);
|
|
676
|
+
}
|
|
677
|
+
|
|
663
678
|
/** Return the display title prepared by the reporting API. */
|
|
664
679
|
export function conversationDisplayTitle(
|
|
665
680
|
conversation: Conversation | undefined,
|