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
3 changes: 3 additions & 0 deletions apps/web/src/components/ChatView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -354,6 +354,7 @@ import { ChatRightPanel } from "./chat/ChatRightPanel";
import { useDraftEnvironmentRouting } from "./chat/useDraftEnvironmentRouting";
import { branchMismatchBannerItem } from "./chat/ThreadActivityBanners";
import { ThreadNow } from "./chat/ThreadNow";
import type { ThreadStructure } from "@t3tools/client-runtime/state/shell";
import { useDelayedStatus } from "../hooks/useDelayedStatus";
import { useTimelineFollow } from "./chat/useTimelineFollow";
import {
Expand All @@ -376,6 +377,7 @@ import type { CodexArtifactTemplate } from "@t3tools/shared/codexArtifactTemplat

const EMPTY_FEEDBACK_SUBMISSIONS: ReadonlyArray<CodexFeedbackSubmission> = [];
const EMPTY_PROVIDER_SKILLS: ServerProvider["skills"] = [];
const EMPTY_TURN_ITEMS: ThreadStructure["turnItems"] = [];
const EMPTY_AUTO_UPDATE_ENVIRONMENTS: ReadonlyArray<EnvironmentPresentation> = [];

const EMPTY_PENDING_FILE_SURFACE_IDS: ReadonlySet<string> = new Set();
Expand Down Expand Up @@ -2405,6 +2407,7 @@ export default function ChatView(props: ChatViewProps) {
runtimeMode,
isWorking,
pendingBackgroundTasks,
turnItems: serverProjection?.turnItems ?? EMPTY_TURN_ITEMS,
activePendingApproval,
activePendingUserInput,
localCheckoutBranchMismatch,
Expand Down
113 changes: 70 additions & 43 deletions apps/web/src/components/chat/ThreadActivityBanners.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,24 @@ import type {
OrchestrationV2ProviderGoal,
ThreadId,
} from "@t3tools/contracts";
import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell";
import type { EnvironmentThreadShell, ThreadStructure } from "@t3tools/client-runtime/state/shell";
import {
presentPendingBackgroundWork,
presentProviderGoal,
} from "@t3tools/client-runtime/state/thread-execution";
import { childThreadInputsTitle } from "@t3tools/client-runtime/state/threads";
import { GitBranchIcon, MessageCircleIcon, ShieldQuestionIcon, TargetIcon } from "lucide-react";
import { formatDuration } from "@t3tools/shared/orchestrationTiming";
import * as DateTime from "effect/DateTime";
import {
CircleDashedIcon,
GitBranchIcon,
MessageCircleIcon,
ShieldQuestionIcon,
TargetIcon,
TerminalIcon,
} from "lucide-react";
import { Fragment } from "react";
import { tickTextContent } from "~/lib/tickTextContent";
import { cn } from "~/lib/utils";
import { Button, InlineButton } from "../ui/button";
import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
Expand Down Expand Up @@ -99,6 +109,7 @@ export function goalNowItem(input: {
export function backgroundWorkNowItem(input: {
readonly threadId: ThreadId;
readonly tasks: ReadonlyArray<OrchestrationV2PendingBackgroundTask>;
readonly turnItems: ThreadStructure["turnItems"];
readonly stopDisabled: boolean;
readonly stopping: boolean;
readonly onStop: () => Promise<void>;
Expand All @@ -107,51 +118,67 @@ export function backgroundWorkNowItem(input: {
const { onOpenThread } = input;
const presentation = presentPendingBackgroundWork(input.tasks);
if (presentation === null) return null;
const rows = presentation.items.map((task) => {
// Same key derivePendingBackgroundWork gives turn-item tasks; subagent items carry progress.
const item = input.turnItems.findLast(
(candidate) => (candidate.nativeItemRef?.nativeId || candidate.id) === task.taskId,
);
const subagent = item?.type === "subagent" ? item : undefined;
return {
task,
activity: subagent?.progress?.replace(/\s+/gu, " ").trim() || null,
startedAtMs: item?.startedAt ? DateTime.toEpochMillis(item.startedAt) : null,
updatedAtMs: subagent ? DateTime.toEpochMillis(subagent.updatedAt) : 0,
childThreadId: task.childThreadId ?? subagent?.childThreadId ?? undefined,
};
});
const [only] = rows.length === 1 ? rows : [];
const newest = rows
.filter((row) => row.activity)
.toSorted((left, right) => right.updatedAtMs - left.updatedAtMs)[0];
const isCommand = (row: (typeof rows)[number]) => row.task.kind !== "subagent";
const verb = presentation.waiting ? "Waiting on" : "Running";
const onlyThreadId = only?.childThreadId;
const startedAtMs = only?.startedAtMs ?? null;
return {
id: `background-work:${input.threadId}`,
icon: (
<span
className={cn(
"size-1.5 rounded-full",
presentation.waiting ? "bg-foreground" : "bg-muted-foreground",
)}
aria-hidden="true"
/>
icon: rows.every(isCommand) ? <TerminalIcon /> : <CircleDashedIcon />,
title: only ? (
<>
{verb} <span className={cn(isCommand(only) && "font-mono")}>{only.task.label}</span>
</>
) : (
presentation.title
),
description: only ? (
Comment thread
macroscopeapp[bot] marked this conversation as resolved.
Resolved
only.activity
) : newest ? (
`${newest.task.label}: ${newest.activity}`
) : rows.every(isCommand) ? (
<span className="font-mono">{rows.map((row) => row.task.label).join(", ")}</span>
) : (
rows.map((row) => row.task.label).join(", ")
),
title: presentation.title,
description:
presentation.items.length === 1 && presentation.items[0]?.childThreadId === undefined
? undefined
: presentation.items.map((item, index) => {
const childThreadId = item.childThreadId;
const separator = index < presentation.items.length - 1 ? "," : null;
return (
<Fragment key={item.taskId}>
{index > 0 ? " " : null}
{childThreadId === undefined ? (
<>
{item.label}
{separator}
</>
) : (
<span className="whitespace-nowrap">
<InlineButton
tone="muted"
aria-label={`${presentation.items.length === 1 ? "Show" : "Open"} subagent ${item.label}`}
onClick={() => onOpenThread(childThreadId)}
>
{presentation.items.length === 1 ? "Show" : item.label}
</InlineButton>
{separator}
</span>
)}
</Fragment>
);
}),
actions: (
<InlineButton disabled={input.stopDisabled} onClick={() => void input.onStop()}>
{input.stopping ? "Stopping..." : "Stop"}
</InlineButton>
<>
{startedAtMs === null ? null : (
<span
ref={tickTextContent(
(nowMs) => formatDuration(Math.max(1_000, nowMs - startedAtMs)),
true,
)}
className="tabular-nums"
/>
)}
{onlyThreadId === undefined ? null : (
<InlineButton tone="muted" onClick={() => onOpenThread(onlyThreadId)}>
Open
</InlineButton>
)}
<InlineButton disabled={input.stopDisabled} onClick={() => void input.onStop()}>
{input.stopping ? "Stopping..." : "Stop"}
</InlineButton>
</>
),
};
}
Expand Down
9 changes: 4 additions & 5 deletions apps/web/src/components/chat/ThreadNow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,15 +54,14 @@ function ThreadNowLine({
{item.description ? (
<>
{" "}
<span aria-hidden>·</span> {item.description}
<span className="ms-0.5 text-muted-foreground/60 light:text-muted-foreground/80">
{item.description}
</span>
</>
) : null}
</NoticeText>
{item.actions ? (
<>
<span aria-hidden>·</span>
<span className="flex shrink-0 items-center gap-1.5">{item.actions}</span>
</>
<span className="ms-1.5 flex shrink-0 items-center gap-2.5">{item.actions}</span>
) : null}
</div>
);
Expand Down
3 changes: 3 additions & 0 deletions apps/web/src/components/chat/useThreadLifecycle.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ interface ThreadLifecycleInput {
readonly runtimeMode: RuntimeMode;
readonly isWorking: boolean;
readonly pendingBackgroundTasks: Parameters<typeof backgroundWorkNowItem>[0]["tasks"];
readonly turnItems: Parameters<typeof backgroundWorkNowItem>[0]["turnItems"];
readonly activePendingApproval: PendingThreadRequests["activePendingApproval"];
readonly activePendingUserInput: PendingThreadRequests["activePendingUserInput"];
readonly localCheckoutBranchMismatch: {
Expand Down Expand Up @@ -132,6 +133,7 @@ export function useThreadLifecycle(input: ThreadLifecycleInput) {
runtimeMode,
isWorking,
pendingBackgroundTasks,
turnItems,
activePendingApproval,
activePendingUserInput,
localCheckoutBranchMismatch,
Expand Down Expand Up @@ -513,6 +515,7 @@ export function useThreadLifecycle(input: ThreadLifecycleInput) {
? backgroundWorkNowItem({
threadId: activeThread.id,
tasks: activeBackgroundTasks,
turnItems,
stopDisabled: !canOperateThread || isStoppingBackgroundWork,
stopping: isStoppingBackgroundWork,
onStop: handleStopBackgroundWorkStable,
Expand Down
Loading