Skip to content
This repository was archived by the owner on Aug 6, 2026. It is now read-only.

Commit 1b48f46

Browse files
refactor(cloud): share session orchestration
Generated-By: PostHog Code Task-Id: 40c57a59-b4e1-4760-8e56-ecd03e9c2f0f
1 parent ab97d0b commit 1b48f46

28 files changed

Lines changed: 3036 additions & 3252 deletions

‎apps/mobile/src/app/task/[id].tsx‎

Lines changed: 33 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import { Text } from "@components/text";
2+
import type {
3+
CloudTaskQueuedMessage,
4+
CloudTaskQueueMoveDirection,
5+
} from "@posthog/core/sessions/cloudTaskQueue";
26
import {
37
countUserMessages,
48
getSessionActivityPhase,
@@ -45,17 +49,18 @@ import {
4549
useQueuedCount,
4650
useToggleMessagingMode,
4751
} from "@/features/tasks/hooks/useMessagingMode";
52+
import { useTaskMessageQueue } from "@/features/tasks/hooks/useTaskMessageQueue";
4853
import { taskKeys } from "@/features/tasks/hooks/useTasks";
49-
import {
50-
type MoveDirection,
51-
type QueuedMessage,
52-
useMessageQueueStore,
53-
} from "@/features/tasks/stores/messageQueueStore";
54+
import { taskMessageQueue } from "@/features/tasks/lib/taskMessageQueue";
55+
import { taskSessionActions } from "@/features/tasks/services/taskSessionService";
5456
import {
5557
pendingTaskPromptStoreApi,
5658
usePendingTaskPrompt,
5759
} from "@/features/tasks/stores/pendingTaskPromptStore";
58-
import { useTaskSessionStore } from "@/features/tasks/stores/taskSessionStore";
60+
import {
61+
getTaskSession,
62+
useTaskSessionStore,
63+
} from "@/features/tasks/stores/taskSessionStore";
5964
import { useTaskStore } from "@/features/tasks/stores/taskStore";
6065
import { confirmStopRun } from "@/features/tasks/utils/archiveGuard";
6166
import { useScreenInsets } from "@/hooks/useScreenInsets";
@@ -69,6 +74,7 @@ import { getPostHogApiClient } from "@/lib/posthogApiClient";
6974
import { useThemeColors } from "@/lib/theme";
7075

7176
const log = logger.scope("task-detail");
77+
type QueuedMessage = CloudTaskQueuedMessage<PendingAttachment>;
7278

7379
function getFirstParam(value?: string | string[]): string | undefined {
7480
return Array.isArray(value) ? value[0] : value;
@@ -107,12 +113,13 @@ export default function TaskDetailScreen() {
107113
sendInterrupting,
108114
sendPermissionResponse,
109115
setConfigOption,
110-
getSessionForTask,
111-
setFocusedTaskId,
112116
steerQueuedMessage,
113117
flushQueuedMessagesIfIdle,
114118
stopRun,
115-
} = useTaskSessionStore();
119+
} = taskSessionActions;
120+
const setFocusedTaskId = useTaskSessionStore(
121+
(state) => state.setFocusedTaskId,
122+
);
116123

117124
useEffect(() => {
118125
if (!taskId) return;
@@ -127,7 +134,7 @@ export default function TaskDetailScreen() {
127134
// Cleared when the screen unmounts. Matches the desktop super-property.
128135
useActiveTaskAnalyticsContext(task?.signal_report ?? null);
129136

130-
const session = taskId ? getSessionForTask(taskId) : undefined;
137+
const session = taskId ? getTaskSession(taskId) : undefined;
131138

132139
// Optimistic echo set by the new-task screen (or the terminal-resume path
133140
// below) so the user's prompt appears in the thread immediately, before
@@ -176,9 +183,7 @@ export default function TaskDetailScreen() {
176183

177184
const messagingMode = useMessagingMode(taskId);
178185
const queuedCount = useQueuedCount(taskId);
179-
const editingQueuedId = useMessageQueueStore((s) =>
180-
taskId ? s.editingByTaskId[taskId] : undefined,
181-
);
186+
const { editingId: editingQueuedId } = useTaskMessageQueue(taskId);
182187
const toggleMessagingMode = useToggleMessagingMode(taskId);
183188
const analytics = useAnalytics();
184189

@@ -369,11 +374,13 @@ export default function TaskDetailScreen() {
369374
// Saving an in-place edit: overwrite the queued message and release the
370375
// drain hold. If the turn already ended while editing, flush now — the
371376
// turn-end drain won't fire again on its own.
372-
const queue = useMessageQueueStore.getState();
373-
const editingId = queue.editingByTaskId[taskId];
377+
const editingId = taskMessageQueue.getSnapshot().editingByTaskId[taskId];
374378
if (editingId) {
375-
queue.update(taskId, editingId, { content: text, attachments });
376-
queue.clearEditing(taskId);
379+
taskMessageQueue.update(taskId, editingId, {
380+
content: text,
381+
attachments,
382+
});
383+
taskMessageQueue.clearEditing(taskId);
377384
flushQueuedMessagesIfIdle(taskId);
378385
return;
379386
}
@@ -395,7 +402,7 @@ export default function TaskDetailScreen() {
395402
// Steer interrupts the turn and resends right away.
396403
if (session?.isPromptPending) {
397404
if (messagingMode === "queue") {
398-
useMessageQueueStore.getState().enqueue(taskId, text, attachments);
405+
taskMessageQueue.enqueue(taskId, text, attachments);
399406
return;
400407
}
401408
sendInterrupting(taskId, text, attachments)
@@ -448,7 +455,7 @@ export default function TaskDetailScreen() {
448455
const handleEditQueued = useCallback(
449456
(message: QueuedMessage) => {
450457
if (!taskId) return;
451-
useMessageQueueStore.getState().setEditing(taskId, message.id);
458+
taskMessageQueue.setEditing(taskId, message.id);
452459
setRestoredDraft({
453460
text: message.content,
454461
attachments: message.attachments,
@@ -459,16 +466,16 @@ export default function TaskDetailScreen() {
459466

460467
const handleCancelEdit = useCallback(() => {
461468
if (!taskId) return;
462-
useMessageQueueStore.getState().clearEditing(taskId);
469+
taskMessageQueue.clearEditing(taskId);
463470
setRestoredDraft({ text: "", attachments: [] });
464471
flushQueuedMessagesIfIdle(taskId);
465472
}, [taskId, flushQueuedMessagesIfIdle]);
466473

467474
const handleMoveQueued = useCallback(
468-
(message: QueuedMessage, direction: MoveDirection) => {
475+
(message: QueuedMessage, direction: CloudTaskQueueMoveDirection) => {
469476
if (!taskId) return;
470477
Haptics.selectionAsync();
471-
useMessageQueueStore.getState().move(taskId, message.id, direction);
478+
taskMessageQueue.move(taskId, message.id, direction);
472479
},
473480
[taskId],
474481
);
@@ -477,8 +484,8 @@ export default function TaskDetailScreen() {
477484
(message: QueuedMessage) => {
478485
if (!taskId) return;
479486
const wasEditing =
480-
useMessageQueueStore.getState().editingByTaskId[taskId] === message.id;
481-
useMessageQueueStore.getState().remove(taskId, message.id);
487+
taskMessageQueue.getSnapshot().editingByTaskId[taskId] === message.id;
488+
taskMessageQueue.remove(taskId, message.id);
482489
if (wasEditing) setRestoredDraft({ text: "", attachments: [] });
483490
},
484491
[taskId],
@@ -525,7 +532,7 @@ export default function TaskDetailScreen() {
525532
const handleStopRun = useCallback(() => {
526533
if (!taskId) return;
527534
confirmStopRun(() => {
528-
const promptsSent = countUserMessages(getSessionForTask(taskId)?.events);
535+
const promptsSent = countUserMessages(getTaskSession(taskId)?.events);
529536
stopRun(taskId)
530537
.then((ok) => {
531538
if (ok) {
@@ -543,7 +550,7 @@ export default function TaskDetailScreen() {
543550
})
544551
.catch(() => {});
545552
});
546-
}, [taskId, stopRun, analytics, getSessionForTask]);
553+
}, [taskId, stopRun, analytics]);
547554

548555
const canStopRun =
549556
!!task &&

‎apps/mobile/src/features/tasks/components/TaskSessionView.tsx‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
import type {
2+
PortableSessionEvent as SessionEvent,
3+
PortableSessionNotification as SessionNotification,
4+
} from "@posthog/core/sessions/portableSessionEvents";
15
import {
26
ArrowDown,
37
Brain,
@@ -25,8 +29,6 @@ import { useThemeColors } from "@/lib/theme";
2529
import type {
2630
CloudPendingPermissionRequest,
2731
PlanEntry,
28-
SessionEvent,
29-
SessionNotification,
3032
SessionNotificationAttachment,
3133
} from "../types";
3234
import { PlanApprovalCard } from "./PlanApprovalCard";

‎apps/mobile/src/features/tasks/composer/QueuedMessagesDock.tsx‎

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import { Text } from "@components/text";
2+
import type {
3+
CloudTaskQueuedMessage,
4+
CloudTaskQueueMoveDirection,
5+
} from "@posthog/core/sessions/cloudTaskQueue";
26
import {
37
CaretDown,
48
CaretUp,
@@ -12,19 +16,21 @@ import { type ReactNode, useState } from "react";
1216
import { Pressable, View } from "react-native";
1317
import { SheetContainer } from "@/components/SheetContainer";
1418
import { useThemeColors } from "@/lib/theme";
15-
import {
16-
type MoveDirection,
17-
type QueuedMessage,
18-
useMessageQueueStore,
19-
} from "../stores/messageQueueStore";
19+
import { useTaskMessageQueue } from "../hooks/useTaskMessageQueue";
20+
import type { PendingAttachment } from "./attachments/types";
21+
22+
type QueuedMessage = CloudTaskQueuedMessage<PendingAttachment>;
2023

2124
interface QueuedMessagesDockProps {
2225
taskId: string;
2326
canSteer: boolean;
2427
onSteer: (message: QueuedMessage) => void;
2528
onEdit: (message: QueuedMessage) => void;
2629
onDiscard: (message: QueuedMessage) => void;
27-
onMove: (message: QueuedMessage, direction: MoveDirection) => void;
30+
onMove: (
31+
message: QueuedMessage,
32+
direction: CloudTaskQueueMoveDirection,
33+
) => void;
2834
}
2935

3036
function previewText(message: QueuedMessage): string {
@@ -42,11 +48,10 @@ export function QueuedMessagesDock({
4248
onMove,
4349
}: QueuedMessagesDockProps) {
4450
const themeColors = useThemeColors();
45-
const queued = useMessageQueueStore((s) => s.queuesByTaskId[taskId]);
46-
const editingId = useMessageQueueStore((s) => s.editingByTaskId[taskId]);
51+
const { messages: queued, editingId } = useTaskMessageQueue(taskId);
4752
const [activeId, setActiveId] = useState<string | null>(null);
4853

49-
if (!queued || queued.length === 0) return null;
54+
if (queued.length === 0) return null;
5055
const active = queued.find((m) => m.id === activeId) ?? null;
5156

5257
return (

‎apps/mobile/src/features/tasks/hooks/useMessagingMode.ts‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
import { useCallback } from "react";
2-
import { useMessageQueueStore } from "../stores/messageQueueStore";
2+
import { taskSessionActions } from "../services/taskSessionService";
33
import {
44
type MessagingMode,
55
useMessagingModeStore,
66
} from "../stores/messagingModeStore";
7-
import { useTaskSessionStore } from "../stores/taskSessionStore";
7+
import { useTaskMessageQueue } from "./useTaskMessageQueue";
88

99
/** Effective mode for a task: per-task override, else the global default. */
1010
export function useMessagingMode(taskId: string | undefined): MessagingMode {
1111
return useMessagingModeStore((s) => s.getEffectiveMode(taskId));
1212
}
1313

1414
export function useQueuedCount(taskId: string | undefined): number {
15-
return useMessageQueueStore((s) => (taskId ? s.getQueue(taskId).length : 0));
15+
return useTaskMessageQueue(taskId ?? "").messages.length;
1616
}
1717

1818
/**
@@ -27,7 +27,7 @@ export function useToggleMessagingMode(taskId: string | undefined): () => void {
2727
const next: MessagingMode = mode === "steer" ? "queue" : "steer";
2828
useMessagingModeStore.getState().setMode(taskId, next);
2929
if (next === "steer") {
30-
void useTaskSessionStore.getState().flushQueuedMessages(taskId);
30+
void taskSessionActions.flushQueuedMessages(taskId);
3131
}
3232
}, [taskId, mode]);
3333
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import type { CloudTaskQueuedMessage } from "@posthog/core/sessions/cloudTaskQueue";
2+
import { useSyncExternalStore } from "react";
3+
import type { PendingAttachment } from "../composer/attachments/types";
4+
import { taskMessageQueue } from "../lib/taskMessageQueue";
5+
6+
interface TaskMessageQueueSelection {
7+
messages: readonly CloudTaskQueuedMessage<PendingAttachment>[];
8+
editingId: string | undefined;
9+
}
10+
11+
export function useTaskMessageQueue(taskId: string): TaskMessageQueueSelection {
12+
const snapshot = useSyncExternalStore(
13+
taskMessageQueue.subscribe,
14+
taskMessageQueue.getSnapshot,
15+
);
16+
return {
17+
messages: snapshot.queuesByTaskId[taskId] ?? [],
18+
editingId: snapshot.editingByTaskId[taskId],
19+
};
20+
}

‎apps/mobile/src/features/tasks/index.ts‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,7 @@ export {
1616
useUpdateTask,
1717
} from "./hooks/useTasks";
1818
// Stores
19-
export {
20-
type TaskSession,
21-
useTaskSessionStore,
22-
} from "./stores/taskSessionStore";
19+
export { useTaskSessionStore } from "./stores/taskSessionStore";
2320
export { useTaskStore } from "./stores/taskStore";
2421

2522
// Types
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { beforeEach, describe, expect, it, vi } from "vitest";
2+
3+
const mocks = vi.hoisted(() => ({
4+
engine: {
5+
off: vi.fn(),
6+
on: vi.fn(),
7+
reconnectIfDisconnected: vi.fn(),
8+
unwatch: vi.fn(),
9+
watch: vi.fn(),
10+
},
11+
}));
12+
13+
vi.mock("@posthog/core/cloud-task/cloud-task-engine", () => ({
14+
createCloudTaskEngine: () => mocks.engine,
15+
}));
16+
17+
vi.mock("@posthog/core/cloud-task/schemas", () => ({
18+
CloudTaskEvent: { Update: "cloud-task-update" },
19+
}));
20+
21+
vi.mock("expo/fetch", () => ({ fetch: vi.fn() }));
22+
23+
vi.mock("@/lib/api", () => ({
24+
authedFetch: vi.fn(),
25+
getBaseUrl: () => "https://app.posthog.test",
26+
getProjectId: () => 42,
27+
}));
28+
29+
vi.mock("@/lib/logger", () => ({
30+
logger: { scope: vi.fn() },
31+
}));
32+
33+
import { watchCloudTask } from "./cloudTaskStream";
34+
35+
describe("watchCloudTask", () => {
36+
beforeEach(() => {
37+
vi.clearAllMocks();
38+
});
39+
40+
it("asks the shared engine to reconnect only when disconnected", () => {
41+
const handle = watchCloudTask({
42+
taskId: "task-1",
43+
runId: "run-1",
44+
onUpdate: vi.fn(),
45+
});
46+
47+
handle.reconnectIfDisconnected();
48+
49+
expect(mocks.engine.reconnectIfDisconnected).toHaveBeenCalledWith(
50+
"task-1",
51+
"run-1",
52+
);
53+
});
54+
});

0 commit comments

Comments
 (0)