Skip to content

Commit 6cf67a2

Browse files
shivamhwpclaude
andcommitted
fix(web): preserve detachable thread controls
Co-Authored-By: Claude <noreply@anthropic.com>
1 parent c0075de commit 6cf67a2

2 files changed

Lines changed: 82 additions & 1 deletion

File tree

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import type { EnvironmentId, ThreadId } from "@t3tools/contracts";
2+
import type { ReactNode } from "react";
3+
import { renderToStaticMarkup } from "react-dom/server";
4+
import { beforeEach, describe, expect, it, vi } from "vite-plus/test";
5+
6+
const testState = vi.hoisted(() => ({
7+
canDetach: false,
8+
relationships: [] as ReadonlyArray<unknown>,
9+
}));
10+
11+
vi.mock("@t3tools/client-runtime/state/thread-relationships", () => ({
12+
deriveThreadRelationshipGraph: () => ({ nodes: new Map() }),
13+
immediateThreadRelationships: () => testState.relationships,
14+
resolveMergeBackTargetThreadId: () => null,
15+
}));
16+
vi.mock("@t3tools/client-runtime/state/thread-workflows", () => ({
17+
canDetachThreadProviderSession: () => testState.canDetach,
18+
}));
19+
vi.mock("@tanstack/react-router", () => ({ useNavigate: () => vi.fn() }));
20+
vi.mock("../../lib/archivedThreadsState", () => ({
21+
useArchivedThreadSnapshots: () => ({ snapshots: [] }),
22+
}));
23+
vi.mock("../../state/entities", () => ({
24+
useThreadProjection: () => ({ projection: { runs: [] } }),
25+
useThreadShells: () => [],
26+
}));
27+
vi.mock("../../state/threads", () => ({
28+
threadEnvironment: {
29+
mergeBack: Symbol("mergeBack"),
30+
stopSession: Symbol("stopSession"),
31+
},
32+
}));
33+
vi.mock("../../state/use-atom-command", () => ({ useAtomCommand: () => vi.fn() }));
34+
vi.mock("../ui/menu", () => ({
35+
Menu: ({ children }: { readonly children: ReactNode }) => <>{children}</>,
36+
MenuTrigger: ({ children }: { readonly children: ReactNode }) => <>{children}</>,
37+
MenuPopup: ({ children }: { readonly children: ReactNode }) => <>{children}</>,
38+
MenuItem: ({ children }: { readonly children: ReactNode }) => <>{children}</>,
39+
}));
40+
41+
import { ThreadRelationshipsPanel } from "./ThreadRelationshipsControl";
42+
43+
const environmentId = "environment:relationships" as EnvironmentId;
44+
const threadId = "thread:relationships" as ThreadId;
45+
const childThreadId = "thread:relationships:subagent" as ThreadId;
46+
47+
const renderPanel = () =>
48+
renderToStaticMarkup(
49+
<ThreadRelationshipsPanel environmentId={environmentId} threadId={threadId} />,
50+
);
51+
52+
describe("ThreadRelationshipsPanel", () => {
53+
beforeEach(() => {
54+
testState.canDetach = false;
55+
testState.relationships = [
56+
{
57+
threadId: childThreadId,
58+
edge: {
59+
kind: "subagent",
60+
sourceThreadId: threadId,
61+
targetThreadId: childThreadId,
62+
status: "running",
63+
},
64+
},
65+
];
66+
});
67+
68+
it("keeps disconnect controls when hidden subagent edges are the only relationships", () => {
69+
testState.canDetach = true;
70+
71+
const markup = renderPanel();
72+
73+
expect(markup).toContain("Lineage");
74+
expect(markup).toContain("Disconnect agent session");
75+
expect(markup).not.toContain("Subagent");
76+
});
77+
78+
it("renders nothing when no visible relationship or detachable session exists", () => {
79+
expect(renderPanel()).toBe("");
80+
});
81+
});

apps/web/src/components/chat/ThreadRelationshipsControl.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ export function ThreadRelationshipsPanel(props: {
124124
const canMerge = mergeTargetThreadId !== null && latestCompletedRun !== null;
125125
const canDetach = projection ? canDetachThreadProviderSession(projection) : false;
126126

127-
if (relationshipRows.length === 0) {
127+
if (relationshipRows.length === 0 && !canDetach) {
128128
return null;
129129
}
130130

0 commit comments

Comments
 (0)