diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts
index fd1fd57481422..bca1d6769cbea 100644
--- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts
+++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.test.ts
@@ -10049,6 +10049,32 @@ describe("ClaudeAdapterV2 background wake turns", () => {
attachments: [],
}),
);
+ yield* Queue.offer(
+ harness.sdkMessages,
+ claudeSdkFrame({
+ type: "assistant",
+ message: {
+ model: "claude-sonnet-4-6",
+ id: "msg_background_command",
+ type: "message",
+ role: "assistant",
+ content: [
+ {
+ type: "tool_use",
+ id: WAKE_TOOL_USE_ID,
+ name: "Bash",
+ input: {
+ command: "python3 audit.py > audit.log 2>&1",
+ run_in_background: true,
+ },
+ },
+ ],
+ },
+ parent_tool_use_id: null,
+ uuid: "background-command-launch",
+ session_id: WAKE_NATIVE_SESSION,
+ }),
+ );
yield* Queue.offer(harness.sdkMessages, wakeTaskStarted);
yield* Queue.offer(harness.sdkMessages, turnOneResult);
yield* harness.awaitTerminal(1);
@@ -10062,6 +10088,37 @@ describe("ClaudeAdapterV2 background wake turns", () => {
WAKE_TASK_ID,
);
+ const command = harness.events.find(
+ (event) =>
+ event.type === "turn_item.updated" &&
+ event.turnItem.type === "command_execution" &&
+ event.turnItem.nativeItemRef?.nativeId === WAKE_TOOL_USE_ID,
+ );
+ assert.equal(command?.type, "turn_item.updated");
+ if (command?.type !== "turn_item.updated") return;
+ const details = {
+ command: "python3 audit.py > audit.log 2>&1",
+ itemId: command.turnItem.id,
+ };
+ assert.deepInclude(
+ afterStart.at(-1)?.providerThread.pendingBackgroundTasks?.[0],
+ details,
+ );
+ yield* harness.offerAndWait(
+ claudeSdkFrame({
+ type: "system",
+ subtype: "background_tasks_changed",
+ tasks: [{ task_id: WAKE_TASK_ID, task_type: "local_bash", description: "Audit" }],
+ uuid: "background-command-snapshot",
+ session_id: WAKE_NATIVE_SESSION,
+ }),
+ );
+ assert.deepInclude(
+ providerThreadRosterEvents(harness.events).at(-1)?.providerThread
+ .pendingBackgroundTasks?.[0],
+ details,
+ );
+
yield* Queue.offer(
harness.sdkMessages,
claudeSdkFrame({
diff --git a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts
index deacdfd617373..5c71e79d98278 100644
--- a/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts
+++ b/apps/server/src/orchestration-v2/Adapters/ClaudeAdapterV2.ts
@@ -5777,7 +5777,12 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
} else {
updated.set(
nativeThreadId,
- new Map(tasks.map((task) => [task.taskId, task] as const)),
+ new Map(
+ tasks.map((task) => {
+ const previous = current.get(nativeThreadId)?.get(task.taskId);
+ return [task.taskId, { ...previous, ...task }] as const;
+ }),
+ ),
);
}
return updated;
@@ -8719,9 +8724,26 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
if (!isClaudeNonSubagentTask(message) || message.is_backgrounded === false) {
return false;
}
- yield* upsertPendingBackgroundTask(
- input.nativeThreadId,
- claudePendingBackgroundTask({
+ const launch =
+ message.tool_use_id === undefined || input.activeContext === null
+ ? undefined
+ : findToolCall(input.activeContext, message.tool_use_id);
+ const command =
+ launch?.classification.itemType === "command_execution"
+ ? firstStringInputField(launch.input, ["command"])
+ : undefined;
+ yield* upsertPendingBackgroundTask(input.nativeThreadId, {
+ ...(launch === undefined || command === undefined
+ ? {}
+ : {
+ itemId: idAllocator.derive.turnItemFromProviderItem({
+ driver: CLAUDE_PROVIDER,
+ nativeItemId: launch.nativeItemId,
+ }),
+ sourceThreadId: launch.threadId,
+ }),
+ ...(command === undefined ? {} : { command }),
+ ...claudePendingBackgroundTask({
taskId: message.task_id,
taskType: claudeTaskTypeFromSdkMessage(message),
startedByMonitor: yield* isClaudeMonitorTask({
@@ -8733,7 +8755,7 @@ export const makeClaudeAdapterV2 = Effect.fn("makeClaudeAdapterV2")(function* (
typeof message.description === "string" ? message.description : undefined,
parentTaskId: Reflect.get(message, "parent_task_id"),
}),
- );
+ });
rosterChanged = true;
} else if (message.type === "system" && message.subtype === "task_notification") {
yield* endClaudeMonitorTasks((taskId) => taskId === message.task_id);
diff --git a/apps/web/src/components/chat/BackgroundWorkDetails.browser.test.tsx b/apps/web/src/components/chat/BackgroundWorkDetails.browser.test.tsx
new file mode 100644
index 0000000000000..01311a60ecf4f
--- /dev/null
+++ b/apps/web/src/components/chat/BackgroundWorkDetails.browser.test.tsx
@@ -0,0 +1,97 @@
+import "~/index.css";
+import { EnvironmentId, ProviderDriverKind, ThreadId, TurnItemId } from "@t3tools/contracts";
+import * as DateTime from "effect/DateTime";
+import { expect, it, vi } from "vite-plus/test";
+import { page } from "vite-plus/test/browser";
+vi.mock(import("./CommandOutputPanel"), () => ({
+ useCommandOutputStreaming: () => true,
+ CommandOutputPanel: () =>
Command output stream
,
+}));
+
+import { render } from "../../../test/render";
+import { backgroundWorkNowItem } from "./ThreadActivityBanners";
+import { ThreadNow } from "./ThreadNow";
+
+it("opens full command details for short labels and keeps pending tasks current", async () => {
+ const command = "python3 audit.py -j 6\n> scratch/audit.log 2>&1";
+ const commandItem = (status: "running" | "completed") => ({
+ id: TurnItemId.make("bash-1"),
+ threadId: ThreadId.make("thread"),
+ runId: null,
+ nodeId: null,
+ providerThreadId: null,
+ providerTurnId: null,
+ nativeItemRef: null,
+ parentItemId: null,
+ ordinal: 1,
+ status,
+ title: null,
+ startedAt: null,
+ completedAt: null,
+ updatedAt: DateTime.makeUnsafe("2026-11-09T00:00:00Z"),
+ type: "command_execution" as const,
+ input: command,
+ output: "Command running in background with ID: native-task",
+ });
+ const view = (
+ description?: string,
+ status: "running" | "completed" = "completed",
+ explicitItem = true,
+ ) => (
+ undefined,
+ onOpenThread: () => undefined,
+ })}
+ />
+ );
+ await using screen = await render(view("Audit"));
+ await screen.getByRole("button", { name: "Details", exact: true }).click();
+ await vi.waitFor(() => expect(page.getByRole("dialog").element().textContent).toContain(command));
+ await expect.element(page.getByText("Command output stream")).not.toBeInTheDocument();
+
+ await screen.rerender(view(command));
+ await expect.element(page.getByRole("heading", { name: "Command", exact: true })).toBeVisible();
+ await screen.rerender(view());
+ await expect.element(page.getByRole("heading", { name: "Command", exact: true })).toBeVisible();
+
+ await screen.rerender(view("Check remaining modules", "running"));
+ await expect
+ .element(page.getByRole("heading", { name: "Check remaining modules" }))
+ .toBeVisible();
+ await expect.element(page.getByText("Command output stream")).toBeVisible();
+ await screen.rerender(view("Native fallback", "completed", false));
+ await expect.element(page.getByText("Command output stream")).toBeVisible();
+ await page.getByRole("button", { name: "Close", exact: true }).click();
+ await expect.element(page.getByRole("dialog")).not.toBeInTheDocument();
+});
diff --git a/apps/web/src/components/chat/BackgroundWorkDetails.tsx b/apps/web/src/components/chat/BackgroundWorkDetails.tsx
new file mode 100644
index 0000000000000..86df01be78f59
--- /dev/null
+++ b/apps/web/src/components/chat/BackgroundWorkDetails.tsx
@@ -0,0 +1,97 @@
+import type {
+ EnvironmentId,
+ OrchestrationV2PendingBackgroundTask,
+ ThreadId,
+} from "@t3tools/contracts";
+import type { ThreadStructure } from "@t3tools/client-runtime/state/shell";
+import { useState } from "react";
+
+import { InlineButton } from "../ui/button";
+import {
+ Dialog,
+ DialogHeader,
+ DialogPanel,
+ DialogPopup,
+ DialogTitle,
+ DialogTrigger,
+} from "../ui/dialog";
+import { CommandOutputPanel, useCommandOutputStreaming } from "./CommandOutputPanel";
+import { ShellCommandBlock } from "./ShellCommandBlock";
+
+export function BackgroundWorkDetails(props: {
+ readonly environmentId: EnvironmentId;
+ readonly threadId: ThreadId;
+ readonly tasks: ReadonlyArray;
+ readonly turnItems: ThreadStructure["turnItems"];
+ readonly onOpenThread: (threadId: ThreadId) => void;
+}) {
+ const [open, setOpen] = useState(false);
+ const streamsOutput = useCommandOutputStreaming(props.environmentId);
+ return (
+
+ );
+}
diff --git a/apps/web/src/components/chat/ThreadActivityBanners.tsx b/apps/web/src/components/chat/ThreadActivityBanners.tsx
index 434094f954a27..b12a6d1ae63b4 100644
--- a/apps/web/src/components/chat/ThreadActivityBanners.tsx
+++ b/apps/web/src/components/chat/ThreadActivityBanners.tsx
@@ -27,6 +27,7 @@ import {
} from "lucide-react";
import { Fragment } from "react";
import { tickTextContent } from "~/lib/tickTextContent";
+import { BackgroundWorkDetails } from "./BackgroundWorkDetails";
import { Button, InlineButton } from "../ui/button";
import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";
import type { ComposerBannerStackItem } from "./ComposerBannerStack";
@@ -168,6 +169,7 @@ export function runStopNowItem(input: {
/** Background commands and subagents, for the end of the timeline. */
export function backgroundWorkNowItem(input: {
+ readonly environmentId: EnvironmentId;
readonly threadId: ThreadId;
readonly tasks: ReadonlyArray;
readonly turnItems: ThreadStructure["turnItems"];
@@ -246,6 +248,13 @@ export function backgroundWorkNowItem(input: {
className="tabular-nums"
/>
)}
+
{onlyThreadId === undefined ? null : (
onOpenThread(onlyThreadId)}>
Open
diff --git a/apps/web/src/components/chat/useThreadLifecycle.tsx b/apps/web/src/components/chat/useThreadLifecycle.tsx
index d2807671f0472..74654e1979a97 100644
--- a/apps/web/src/components/chat/useThreadLifecycle.tsx
+++ b/apps/web/src/components/chat/useThreadLifecycle.tsx
@@ -596,6 +596,7 @@ export function useThreadLifecycle(input: ThreadLifecycleInput) {
: null;
const backgroundWorkNow = activeThread
? backgroundWorkNowItem({
+ environmentId,
threadId: activeThread.id,
tasks: activeBackgroundTasks,
turnItems,
diff --git a/packages/contracts/src/orchestrationV2.ts b/packages/contracts/src/orchestrationV2.ts
index ca72b708ad6e4..856cf89bafcb6 100644
--- a/packages/contracts/src/orchestrationV2.ts
+++ b/packages/contracts/src/orchestrationV2.ts
@@ -1023,6 +1023,9 @@ function kindUnionWithFallback<
}
const PendingBackgroundTaskFields = {
+ command: Schema.optional(Schema.String),
+ itemId: Schema.optional(TurnItemId),
+ sourceThreadId: Schema.optional(ThreadId),
taskId: TrimmedNonEmptyString,
description: Schema.optional(TrimmedNonEmptyString),
/** The subagent task that started this work and waits on it. */