Skip to content

Commit ecde404

Browse files
committed
fix: handle thread archive/unarchive app server events
1 parent 818a03a commit ecde404

8 files changed

Lines changed: 155 additions & 2 deletions

File tree

docs/app-server-events.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,9 @@ routed in `useAppServerEvents.ts` or handled in feature-specific subscriptions.
5959
- `item/agentMessage/delta`
6060
- `turn/started`
6161
- `thread/started`
62+
- `thread/archived`
6263
- `thread/name/updated`
64+
- `thread/unarchived`
6365
- `codex/backgroundThread`
6466
- `error`
6567
- `turn/completed`
@@ -103,9 +105,7 @@ events are currently not routed:
103105
- `item/mcpToolCall/progress`
104106
- `mcpServer/oauthLogin/completed`
105107
- `model/rerouted`
106-
- `thread/archived`
107108
- `thread/compacted` (deprecated; intentionally not routed)
108-
- `thread/unarchived`
109109
- `deprecationNotice`
110110
- `configWarning`
111111
- `windows/worldWritableWarning`

src/features/app/hooks/useAppServerEvents.test.tsx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ describe("useAppServerEvents", () => {
4949
onWorkspaceConnected: vi.fn(),
5050
onThreadStarted: vi.fn(),
5151
onThreadNameUpdated: vi.fn(),
52+
onThreadArchived: vi.fn(),
53+
onThreadUnarchived: vi.fn(),
5254
onBackgroundThreadAction: vi.fn(),
5355
onAgentMessageDelta: vi.fn(),
5456
onReasoningSummaryBoundary: vi.fn(),
@@ -144,6 +146,28 @@ describe("useAppServerEvents", () => {
144146
threadName: "Renamed from server",
145147
});
146148

149+
act(() => {
150+
listener?.({
151+
workspace_id: "ws-1",
152+
message: {
153+
method: "thread/archived",
154+
params: { thread_id: "thread-2" },
155+
},
156+
});
157+
});
158+
expect(handlers.onThreadArchived).toHaveBeenCalledWith("ws-1", "thread-2");
159+
160+
act(() => {
161+
listener?.({
162+
workspace_id: "ws-1",
163+
message: {
164+
method: "thread/unarchived",
165+
params: { threadId: "thread-2" },
166+
},
167+
});
168+
});
169+
expect(handlers.onThreadUnarchived).toHaveBeenCalledWith("ws-1", "thread-2");
170+
147171
act(() => {
148172
listener?.({
149173
workspace_id: "ws-1",

src/features/app/hooks/useAppServerEvents.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ type AppServerEventHandlers = {
3535
workspaceId: string,
3636
payload: { threadId: string; threadName: string | null },
3737
) => void;
38+
onThreadArchived?: (workspaceId: string, threadId: string) => void;
39+
onThreadUnarchived?: (workspaceId: string, threadId: string) => void;
3840
onBackgroundThreadAction?: (
3941
workspaceId: string,
4042
threadId: string,
@@ -108,9 +110,11 @@ export const METHODS_ROUTED_IN_USE_APP_SERVER_EVENTS = [
108110
"item/reasoning/textDelta",
109111
"item/started",
110112
"item/tool/requestUserInput",
113+
"thread/archived",
111114
"thread/name/updated",
112115
"thread/started",
113116
"thread/tokenUsage/updated",
117+
"thread/unarchived",
114118
"turn/completed",
115119
"turn/diff/updated",
116120
"turn/plan/updated",
@@ -248,6 +252,22 @@ export function useAppServerEvents(handlers: AppServerEventHandlers) {
248252
return;
249253
}
250254

255+
if (method === "thread/archived") {
256+
const threadId = String(params.threadId ?? params.thread_id ?? "").trim();
257+
if (threadId) {
258+
currentHandlers.onThreadArchived?.(workspace_id, threadId);
259+
}
260+
return;
261+
}
262+
263+
if (method === "thread/unarchived") {
264+
const threadId = String(params.threadId ?? params.thread_id ?? "").trim();
265+
if (threadId) {
266+
currentHandlers.onThreadUnarchived?.(workspace_id, threadId);
267+
}
268+
return;
269+
}
270+
251271
if (method === "codex/backgroundThread") {
252272
const threadId = String(params.threadId ?? params.thread_id ?? "");
253273
const action = String(params.action ?? "hide");

src/features/threads/hooks/useThreadEventHandlers.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,8 @@ export function useThreadEventHandlers({
9797
const {
9898
onThreadStarted,
9999
onThreadNameUpdated,
100+
onThreadArchived,
101+
onThreadUnarchived,
100102
onTurnStarted,
101103
onTurnCompleted,
102104
onTurnPlanUpdated,
@@ -164,6 +166,8 @@ export function useThreadEventHandlers({
164166
onFileChangeOutputDelta,
165167
onThreadStarted,
166168
onThreadNameUpdated,
169+
onThreadArchived,
170+
onThreadUnarchived,
167171
onTurnStarted,
168172
onTurnCompleted,
169173
onTurnPlanUpdated,
@@ -191,6 +195,8 @@ export function useThreadEventHandlers({
191195
onFileChangeOutputDelta,
192196
onThreadStarted,
193197
onThreadNameUpdated,
198+
onThreadArchived,
199+
onThreadUnarchived,
194200
onTurnStarted,
195201
onTurnCompleted,
196202
onTurnPlanUpdated,

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

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,48 @@ describe("useThreadTurnEvents", () => {
207207
);
208208
});
209209

210+
it("removes thread state on thread archived", () => {
211+
const { result, dispatch } = makeOptions();
212+
213+
act(() => {
214+
result.current.onThreadArchived("ws-1", "thread-7");
215+
});
216+
217+
expect(dispatch).toHaveBeenCalledWith({
218+
type: "removeThread",
219+
workspaceId: "ws-1",
220+
threadId: "thread-7",
221+
});
222+
});
223+
224+
it("re-adds thread summary on thread unarchived", () => {
225+
const { result, dispatch, recordThreadActivity, safeMessageActivity } =
226+
makeOptions();
227+
228+
act(() => {
229+
result.current.onThreadUnarchived("ws-1", "thread-8");
230+
});
231+
232+
expect(dispatch).toHaveBeenCalledWith({
233+
type: "ensureThread",
234+
workspaceId: "ws-1",
235+
threadId: "thread-8",
236+
});
237+
expect(dispatch).toHaveBeenCalledWith(
238+
expect.objectContaining({
239+
type: "setThreadTimestamp",
240+
workspaceId: "ws-1",
241+
threadId: "thread-8",
242+
}),
243+
);
244+
expect(recordThreadActivity).toHaveBeenCalledWith(
245+
"ws-1",
246+
"thread-8",
247+
expect.any(Number),
248+
);
249+
expect(safeMessageActivity).toHaveBeenCalled();
250+
});
251+
210252
it("marks processing and active turn on turn started", () => {
211253
const { result, dispatch, markProcessing, setActiveTurnId } = makeOptions();
212254

src/features/threads/hooks/useThreadTurnEvents.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,44 @@ export function useThreadTurnEvents({
149149
[dispatch, getCustomName],
150150
);
151151

152+
const onThreadArchived = useCallback(
153+
(workspaceId: string, threadId: string) => {
154+
if (!threadId) {
155+
return;
156+
}
157+
dispatch({ type: "removeThread", workspaceId, threadId });
158+
},
159+
[dispatch],
160+
);
161+
162+
const onThreadUnarchived = useCallback(
163+
(workspaceId: string, threadId: string) => {
164+
if (!threadId) {
165+
return;
166+
}
167+
dispatch({ type: "ensureThread", workspaceId, threadId });
168+
const customName = getCustomName(workspaceId, threadId);
169+
if (customName) {
170+
dispatch({
171+
type: "setThreadName",
172+
workspaceId,
173+
threadId,
174+
name: customName,
175+
});
176+
}
177+
const timestamp = Date.now();
178+
dispatch({
179+
type: "setThreadTimestamp",
180+
workspaceId,
181+
threadId,
182+
timestamp,
183+
});
184+
recordThreadActivity(workspaceId, threadId, timestamp);
185+
safeMessageActivity();
186+
},
187+
[dispatch, getCustomName, recordThreadActivity, safeMessageActivity],
188+
);
189+
152190
const onTurnStarted = useCallback(
153191
(workspaceId: string, threadId: string, turnId: string) => {
154192
dispatch({
@@ -293,6 +331,8 @@ export function useThreadTurnEvents({
293331
return {
294332
onThreadStarted,
295333
onThreadNameUpdated,
334+
onThreadArchived,
335+
onThreadUnarchived,
296336
onTurnStarted,
297337
onTurnCompleted,
298338
onTurnPlanUpdated,

src/features/threads/hooks/useThreads.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -402,16 +402,35 @@ export function useThreads({
402402
[onSubagentThreadDetected, threadHandlers, updateThreadParent],
403403
);
404404

405+
const handleThreadArchived = useCallback(
406+
(workspaceId: string, threadId: string) => {
407+
threadHandlers.onThreadArchived?.(workspaceId, threadId);
408+
unpinThread(workspaceId, threadId);
409+
},
410+
[threadHandlers, unpinThread],
411+
);
412+
413+
const handleThreadUnarchived = useCallback(
414+
(workspaceId: string, threadId: string) => {
415+
threadHandlers.onThreadUnarchived?.(workspaceId, threadId);
416+
},
417+
[threadHandlers],
418+
);
419+
405420
const handlers = useMemo(
406421
() => ({
407422
...threadHandlers,
408423
onThreadStarted: handleThreadStarted,
424+
onThreadArchived: handleThreadArchived,
425+
onThreadUnarchived: handleThreadUnarchived,
409426
onAccountUpdated: handleAccountUpdated,
410427
onAccountLoginCompleted: handleAccountLoginCompleted,
411428
}),
412429
[
413430
threadHandlers,
414431
handleThreadStarted,
432+
handleThreadArchived,
433+
handleThreadUnarchived,
415434
handleAccountUpdated,
416435
handleAccountLoginCompleted,
417436
],

src/utils/appServerEvents.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ export const SUPPORTED_APP_SERVER_METHODS = [
2020
"item/reasoning/textDelta",
2121
"item/started",
2222
"item/tool/requestUserInput",
23+
"thread/archived",
2324
"thread/name/updated",
2425
"thread/started",
2526
"thread/tokenUsage/updated",
27+
"thread/unarchived",
2628
"turn/completed",
2729
"turn/diff/updated",
2830
"turn/plan/updated",

0 commit comments

Comments
 (0)