Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,793 changes: 710 additions & 1,083 deletions apps/web/src/components/Sidebar.tsx

Large diffs are not rendered by default.

360 changes: 313 additions & 47 deletions apps/web/src/components/ThreadHoverCard.tsx

Large diffs are not rendered by default.

64 changes: 53 additions & 11 deletions apps/web/src/components/chat/AgentFleetRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
* - The Agents panel's details toggle hides every row's second line (`agentListViewStore.ts`).
* - Only a working row on screen (or an open preview) subscribes to its child thread.
*/
import { scopeThreadRef } from "@t3tools/client-runtime/environment";
import { scopeProjectRef, scopeThreadRef } from "@t3tools/client-runtime/environment";
import {
subagentFromRecord,
subagentIdentityParts,
Expand All @@ -37,7 +37,8 @@ import type {
ScopedThreadRef,
TurnItemId,
} from "@t3tools/contracts";
import { CheckIcon, CornerDownRightIcon, XIcon } from "lucide-react";
import { resolveSubagentMetadata } from "@t3tools/client-runtime/state/subagent-display";
import { CheckIcon, CornerDownRightIcon, FolderIcon, GitBranchIcon, XIcon } from "lucide-react";
import { useRef, type MouseEvent, type ReactNode } from "react";

import { useAgentListViewStore } from "~/agentListViewStore";
Expand All @@ -49,17 +50,19 @@ import {
shouldShowInstanceBadge,
type ProviderInstanceEntry,
} from "~/providerInstances";
import { useThreadProjection, useThreadShells } from "~/state/entities";
import { useProject, useThreadProjection, useThreadShell, useThreadShells } from "~/state/entities";
import { useEnvironmentProviders } from "~/state/environments";
import { formatSecondsTimestamp } from "~/timestampFormat";

import { AgentElapsed, STATUS_VISUALS, StatusDot } from "../AgentStatus";
import { MiddleTruncate } from "../ui/middle-truncate";
import { PreviewCard, PreviewCardPopup, PreviewCardTrigger } from "../ui/preview-card";
import { AgentUsageFooter, ToolCallList } from "./AgentActivityParts";
import { AgentHandle } from "./AgentHandle";
import { showAgentInPanel } from "./agentContextMenu";
import { CursorPreviewCard } from "./CursorPreviewCard";
import { ProviderInstanceIcon } from "./ProviderInstanceIcon";
import { SubagentModelLabel } from "./SubagentModelLabel";
import { SubagentActivityLine } from "./SubagentActivityLine";

export type ProviderEntries = ReadonlyMap<ProviderInstanceId, ProviderInstanceEntry>;
Expand Down Expand Up @@ -154,11 +157,23 @@ export function AgentPreviewContent(props: {
const totalCalls = Math.max(calls.length, agent.usage?.toolUses ?? 0);
const outcome = agent.error ?? (live ? null : agent.result);
const prompt = record?.prompt.trim() || null;
const identity = [
STATUS_VISUALS[agent.status].label,
...(props.provider ? [props.provider.displayName] : []),
...subagentIdentityParts(agent, runStats.runs),
];
const ownerShell = useThreadShell(
scopeThreadRef(props.parentRef.environmentId, entry.ownerThreadId),
)?.source;
const ownerProject = useProject(
ownerShell ? scopeProjectRef(props.parentRef.environmentId, ownerShell.projectId) : null,
);
const agentProject = useProject(
entry.shell ? scopeProjectRef(props.parentRef.environmentId, entry.shell.projectId) : null,
);
// Workspace lines appear only where the agent works somewhere its owner doesn't.
const { workspace } = resolveSubagentMetadata({
model: agent.model,
parentThread: ownerShell,
childThread: entry.shell,
parentProject: ownerProject,
childProject: agentProject,
});
return (
<div className="flex cursor-pointer flex-col" onClick={() => props.onOpen()}>
<div className="flex flex-col gap-0.5 border-b border-border/60 px-3 pt-2.5 pb-2">
Expand All @@ -169,14 +184,41 @@ export function AgentPreviewContent(props: {
</span>
</div>
<div className="flex min-w-0 items-center gap-2 ps-3.5">
<AgentHandle handle={entry.handle} copyable />
{entry.handle ? <AgentHandle handle={entry.handle} copyable /> : null}
<span className="ms-auto shrink-0 font-mono text-2xs text-muted-foreground">
<AgentElapsed agent={agent} />
</span>
</div>
<p className="truncate ps-3.5 font-mono text-2xs text-muted-foreground">
{identity.join(" · ")}
<p className="flex min-w-0 items-center gap-1 ps-3.5 font-mono text-2xs whitespace-nowrap text-muted-foreground">
<span className="shrink-0">
{[STATUS_VISUALS[agent.status].label, props.provider?.displayName]
.filter(Boolean)
.join(" · ")}{" "}
·
</span>
<SubagentModelLabel
model={agent.model}
providerInstanceId={entry.providerInstanceId}
origin={record?.origin ?? entry.subagent?.origin ?? null}
modelSelection={record?.modelSelection}
childThread={entry.shell}
provider={props.provider?.snapshot}
/>
{runStats.runs > 1 ? <span className="shrink-0">· run {runStats.runs}</span> : null}
</p>
{workspace.map(({ label, value }) => {
const Icon = label === "Branch" ? GitBranchIcon : FolderIcon;
return (
<div
key={label}
className="flex min-w-0 items-center gap-1.5 ps-3.5 font-mono text-2xs text-muted-foreground"
>
<Icon aria-hidden className="size-3 shrink-0" />
<span className="sr-only">{label}</span>
<MiddleTruncate value={value} className="flex" showTitle={false} />
</div>
);
})}
</div>
<div className="flex flex-col gap-2.5 px-2.5 py-2">
{live && agent.progress ? (
Expand Down
3 changes: 2 additions & 1 deletion apps/web/src/components/chat/MessagesTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2272,10 +2272,11 @@ function MessageAuthorHeading({ children }: { children: string }) {

function UserMessageRow({ row }: { row: Extract<TimelineRow, { kind: "message" }> }) {
const ctx = use(TimelineRowCtx);
return ctx.workflowOpening?.messageId === row.message.id ? (
return ctx.workflowOpening?.messageId === row.message.id && ctx.threadRef !== null ? (
<WorkflowOpeningBlock
opening={ctx.workflowOpening}
environmentId={ctx.activeThreadEnvironmentId}
ownerThreadId={ctx.threadRef.threadId}
onOpenThread={(threadId) => ctx.onOpenThread(ThreadId.make(threadId))}
/>
) : (
Expand Down
91 changes: 91 additions & 0 deletions apps/web/src/components/chat/SubagentModelLabel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { resolveSubagentMetadata } from "@t3tools/client-runtime/state/subagent-display";
import { REASONING_EFFORT_OPTION_IDS } from "@t3tools/client-runtime/state/thread-execution";
import type {
OrchestrationV2Subagent,
OrchestrationV2ThreadShell,
ProviderInstanceId,
ServerProvider,
} from "@t3tools/contracts";
import { getModelSelectionStringOptionValue, resolveSelectableModel } from "@t3tools/shared/model";

import { getTraitsSpeedDisplay, TraitsSpeedIcon } from "./TraitsSpeed";

/**
* An agent's model as the catalog names it, with the effort and speed it was started with.
* Those come from the agent's own selection, or its thread's for an app-owned agent, and only
* when that selection is for the model it reports running.
*/
export function SubagentModelLabel(props: {
model: string | null;
providerInstanceId: ProviderInstanceId;
origin: OrchestrationV2Subagent["origin"] | null;
modelSelection: OrchestrationV2Subagent["modelSelection"] | undefined;
childThread: Pick<OrchestrationV2ThreadShell, "modelSelection"> | null;
provider: ServerProvider | undefined;
}) {
const { provider } = props;
const { modelLabel } = resolveSubagentMetadata({ model: props.model, provider });
const model = props.model?.trim();
const modelSlug = provider
? resolveSelectableModel(provider.driver, model, provider.models)
: model;
const providerModel = provider?.models.find((candidate) => candidate.slug === modelSlug);
const childSelection =
props.modelSelection ??
(props.origin === "app_owned" ? props.childThread?.modelSelection : undefined);
const childModel = provider
? (resolveSelectableModel(provider.driver, childSelection?.model, provider.models) ??
childSelection?.model.trim())
: childSelection?.model.trim();
const matchingSelection =
childModel === (modelSlug ?? model) && childSelection?.instanceId === props.providerInstanceId
? childSelection
: undefined;
const effort = REASONING_EFFORT_OPTION_IDS.map((id) =>
getModelSelectionStringOptionValue(matchingSelection, id),
).find(Boolean);
const speed = provider
? providerModel?.capabilities?.optionDescriptors
?.map((descriptor) => {
const saved = matchingSelection?.options?.find((option) => option.id === descriptor.id);
if (
descriptor.id === "fastMode" &&
descriptor.type === "boolean" &&
typeof saved?.value === "boolean"
) {
return getTraitsSpeedDisplay(provider.driver, {
...descriptor,
currentValue: saved.value,
});
}
if (
provider.driver === "codex" &&
descriptor.id === "serviceTier" &&
descriptor.type === "select" &&
typeof saved?.value === "string" &&
descriptor.options.some((option) => option.id === saved.value)
) {
return getTraitsSpeedDisplay(provider.driver, {
...descriptor,
currentValue: saved.value,
});
}
return null;
})
.find(Boolean)
: undefined;
return (
<span className="inline-flex min-w-0 items-center gap-1">
<span className="min-w-0 truncate">{modelLabel}</span>
{effort || speed?.speedIcon ? (
<span className="inline-flex shrink-0 items-center gap-1">
{effort ? " · " : null}
{speed?.speedIcon && provider ? (
<TraitsSpeedIcon provider={provider.driver} speedIcon={speed.speedIcon} size="xs" />
) : null}
{effort}
</span>
) : null}
</span>
);
}
Loading