Skip to content

Commit de54a23

Browse files
committed
fix(threads): hide memory consolidation subagent threads from sidebar
1 parent f3fa87e commit de54a23

File tree

6 files changed

+163
-11
lines changed

6 files changed

+163
-11
lines changed

src/features/threads/hooks/useThreadActions.test.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1185,6 +1185,48 @@ describe("useThreadActions", () => {
11851185
});
11861186
});
11871187

1188+
it("hides memory consolidation subagent threads from thread/list", async () => {
1189+
vi.mocked(listThreads).mockResolvedValue({
1190+
result: {
1191+
data: [
1192+
{
1193+
id: "memory-thread",
1194+
cwd: "/tmp/codex",
1195+
preview: "Memory helper",
1196+
updated_at: 4500,
1197+
source: {
1198+
subagent: "memory_consolidation",
1199+
},
1200+
},
1201+
],
1202+
nextCursor: null,
1203+
},
1204+
});
1205+
vi.mocked(getThreadTimestamp).mockImplementation((thread) => {
1206+
const value = (thread as Record<string, unknown>).updated_at as number;
1207+
return value ?? 0;
1208+
});
1209+
1210+
const { result, dispatch } = renderActions();
1211+
1212+
await act(async () => {
1213+
await result.current.listThreadsForWorkspace(workspace);
1214+
});
1215+
1216+
expect(dispatch).toHaveBeenCalledWith({
1217+
type: "hideThread",
1218+
workspaceId: "ws-1",
1219+
threadId: "memory-thread",
1220+
});
1221+
expect(dispatch).toHaveBeenCalledWith({
1222+
type: "setThreads",
1223+
workspaceId: "ws-1",
1224+
sortKey: "updated_at",
1225+
preserveAnchors: true,
1226+
threads: [],
1227+
});
1228+
});
1229+
11881230
it("matches windows workspace threads client-side", async () => {
11891231
const windowsWorkspace: WorkspaceInfo = {
11901232
...workspace,

src/features/threads/hooks/useThreadActions.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
getParentThreadIdFromThread,
3333
getResumedTurnState,
3434
isSubagentThreadSource,
35+
shouldHideSubagentThreadFromSidebar,
3536
} from "@threads/utils/threadRpc";
3637
import { saveThreadActivity } from "@threads/utils/threadStorage";
3738
import type { ThreadAction, ThreadState } from "./useThreadsReducer";
@@ -522,6 +523,9 @@ export function useThreadActions({
522523
: preview
523524
: fallbackName;
524525
const metadata = extractThreadCodexMetadata(thread);
526+
if (shouldHideSubagentThreadFromSidebar(thread.source)) {
527+
return null;
528+
}
525529
const isSubagent = isSubagentThreadSource(thread.source);
526530
return {
527531
id,
@@ -633,8 +637,12 @@ export function useThreadActions({
633637
if (!workspaceId) {
634638
return;
635639
}
636-
matchingThreadsByWorkspace[workspaceId]?.push(thread);
637640
const threadId = String(thread?.id ?? "");
641+
if (threadId && shouldHideSubagentThreadFromSidebar(thread.source)) {
642+
dispatch({ type: "hideThread", workspaceId, threadId });
643+
return;
644+
}
645+
matchingThreadsByWorkspace[workspaceId]?.push(thread);
638646
if (!threadId) {
639647
return;
640648
}
@@ -915,7 +923,15 @@ export function useThreadActions({
915923
workspacePathLookup,
916924
allowedWorkspaceIds,
917925
);
918-
return workspaceId === workspace.id;
926+
if (workspaceId !== workspace.id) {
927+
return false;
928+
}
929+
const threadId = String(thread?.id ?? "");
930+
if (threadId && shouldHideSubagentThreadFromSidebar(thread.source)) {
931+
dispatch({ type: "hideThread", workspaceId, threadId });
932+
return false;
933+
}
934+
return true;
919935
},
920936
),
921937
);

src/features/threads/hooks/useThreadTurnEvents.test.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ describe("useThreadTurnEvents", () => {
174174
expect(safeMessageActivity).not.toHaveBeenCalled();
175175
});
176176

177-
it("ignores parentless subagent thread started events without hiding them", () => {
177+
it("hides memory consolidation thread started events", () => {
178178
const { result, dispatch, recordThreadActivity, safeMessageActivity } =
179179
makeOptions();
180180

@@ -194,13 +194,11 @@ describe("useThreadTurnEvents", () => {
194194
threadId: "thread-subagent-orphan",
195195
}),
196196
);
197-
expect(dispatch).not.toHaveBeenCalledWith(
198-
expect.objectContaining({
199-
type: "hideThread",
200-
workspaceId: "ws-1",
201-
threadId: "thread-subagent-orphan",
202-
}),
203-
);
197+
expect(dispatch).toHaveBeenCalledWith({
198+
type: "hideThread",
199+
workspaceId: "ws-1",
200+
threadId: "thread-subagent-orphan",
201+
});
204202
expect(recordThreadActivity).not.toHaveBeenCalled();
205203
expect(safeMessageActivity).not.toHaveBeenCalled();
206204
});

src/features/threads/hooks/useThreadTurnEvents.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { looksAutoGeneratedThreadName } from "@threads/utils/threadNaming";
1313
import {
1414
getParentThreadIdFromThread,
1515
isSubagentThreadSource,
16+
shouldHideSubagentThreadFromSidebar,
1617
} from "@threads/utils/threadRpc";
1718
import type { ThreadAction } from "./useThreadsReducer";
1819

@@ -121,6 +122,10 @@ export function useThreadTurnEvents({
121122
if (isThreadHidden(workspaceId, threadId)) {
122123
return;
123124
}
125+
if (shouldHideSubagentThreadFromSidebar(thread.source)) {
126+
dispatch({ type: "hideThread", workspaceId, threadId });
127+
return;
128+
}
124129
const sourceParentId = getParentThreadIdFromThread(thread);
125130
if (isSubagentThreadSource(thread.source) && !sourceParentId) {
126131
// Some thread/started payloads omit parent metadata initially.
@@ -242,7 +247,13 @@ export function useThreadTurnEvents({
242247
setActiveTurnId(threadId, turnId);
243248
}
244249
},
245-
[dispatch, getActiveTurnId, markProcessing, pendingInterruptsRef, setActiveTurnId],
250+
[
251+
dispatch,
252+
getActiveTurnId,
253+
markProcessing,
254+
pendingInterruptsRef,
255+
setActiveTurnId,
256+
],
246257
);
247258

248259
const onTurnCompleted = useCallback(

src/features/threads/utils/threadRpc.test.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
getResumedActiveTurnId,
55
getResumedTurnState,
66
isSubagentThreadSource,
7+
shouldHideSubagentThreadFromSidebar,
78
} from "./threadRpc";
89

910
describe("threadRpc", () => {
@@ -127,4 +128,20 @@ describe("threadRpc", () => {
127128
expect(isSubagentThreadSource("vscode")).toBe(false);
128129
expect(isSubagentThreadSource({})).toBe(false);
129130
});
131+
132+
it("hides only memory consolidation subagents from sidebar", () => {
133+
expect(
134+
shouldHideSubagentThreadFromSidebar({ subagent: "memory_consolidation" }),
135+
).toBe(true);
136+
expect(
137+
shouldHideSubagentThreadFromSidebar({ subAgent: { memory_consolidation: true } }),
138+
).toBe(true);
139+
expect(shouldHideSubagentThreadFromSidebar("subagent_memory_consolidation")).toBe(
140+
true,
141+
);
142+
expect(shouldHideSubagentThreadFromSidebar({ subAgent: { review: true } })).toBe(
143+
false,
144+
);
145+
expect(shouldHideSubagentThreadFromSidebar("subagent_review")).toBe(false);
146+
});
130147
});

src/features/threads/utils/threadRpc.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,72 @@
11
import { asString } from "./threadNormalize";
22

3+
const SIDEBAR_HIDDEN_SUBAGENT_KINDS = new Set(["memory_consolidation"]);
4+
35
function asRecord(value: unknown): Record<string, unknown> | null {
46
if (!value || typeof value !== "object") {
57
return null;
68
}
79
return value as Record<string, unknown>;
810
}
911

12+
function normalizeSubagentKind(value: string): string {
13+
const normalized = value
14+
.trim()
15+
.toLowerCase()
16+
.replace(/[\s-]/g, "_");
17+
if (normalized.startsWith("subagent_")) {
18+
return normalized.slice("subagent_".length);
19+
}
20+
if (normalized.startsWith("sub_agent_")) {
21+
return normalized.slice("sub_agent_".length);
22+
}
23+
return normalized;
24+
}
25+
26+
function getSubagentKind(source: unknown): string | null {
27+
if (typeof source === "string") {
28+
const normalized = normalizeSubagentKind(source);
29+
return normalized || null;
30+
}
31+
32+
const sourceRecord = asRecord(source);
33+
if (!sourceRecord) {
34+
return null;
35+
}
36+
37+
const subAgentRaw =
38+
sourceRecord.subAgent ?? sourceRecord.sub_agent ?? sourceRecord.subagent;
39+
if (typeof subAgentRaw === "string") {
40+
const normalized = normalizeSubagentKind(subAgentRaw);
41+
return normalized || null;
42+
}
43+
44+
const subAgentRecord = asRecord(subAgentRaw);
45+
if (!subAgentRecord) {
46+
return null;
47+
}
48+
49+
const explicitKind = asString(
50+
subAgentRecord.kind ??
51+
subAgentRecord.type ??
52+
subAgentRecord.name ??
53+
subAgentRecord.id,
54+
);
55+
if (explicitKind) {
56+
const normalized = normalizeSubagentKind(explicitKind);
57+
return normalized || null;
58+
}
59+
60+
const candidateKeys = Object.keys(subAgentRecord).filter(
61+
(key) => key !== "thread_spawn" && key !== "threadSpawn",
62+
);
63+
if (candidateKeys.length !== 1) {
64+
return null;
65+
}
66+
const normalized = normalizeSubagentKind(candidateKeys[0] ?? "");
67+
return normalized || null;
68+
}
69+
1070
export function isSubagentThreadSource(source: unknown): boolean {
1171
if (typeof source === "string") {
1272
const normalized = source.trim().toLowerCase();
@@ -29,6 +89,14 @@ export function isSubagentThreadSource(source: unknown): boolean {
2989
return typeof subAgent === "object";
3090
}
3191

92+
export function shouldHideSubagentThreadFromSidebar(source: unknown): boolean {
93+
const subagentKind = getSubagentKind(source);
94+
if (!subagentKind) {
95+
return false;
96+
}
97+
return SIDEBAR_HIDDEN_SUBAGENT_KINDS.has(subagentKind);
98+
}
99+
32100
export function getParentThreadIdFromSource(source: unknown): string | null {
33101
const sourceRecord = asRecord(source);
34102
if (!sourceRecord) {

0 commit comments

Comments
 (0)