Skip to content

Commit 2dafbcd

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

28 files changed

Lines changed: 3047 additions & 3245 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 { useQueryClient } from "@tanstack/react-query";
37
import * as Haptics from "expo-haptics";
48
import { useLocalSearchParams, useRouter } from "expo-router";
@@ -39,17 +43,18 @@ import {
3943
useQueuedCount,
4044
useToggleMessagingMode,
4145
} from "@/features/tasks/hooks/useMessagingMode";
46+
import { useTaskMessageQueue } from "@/features/tasks/hooks/useTaskMessageQueue";
4247
import { taskKeys } from "@/features/tasks/hooks/useTasks";
43-
import {
44-
type MoveDirection,
45-
type QueuedMessage,
46-
useMessageQueueStore,
47-
} from "@/features/tasks/stores/messageQueueStore";
48+
import { taskMessageQueue } from "@/features/tasks/lib/taskMessageQueue";
49+
import { taskSessionActions } from "@/features/tasks/services/taskSessionService";
4850
import {
4951
pendingTaskPromptStoreApi,
5052
usePendingTaskPrompt,
5153
} from "@/features/tasks/stores/pendingTaskPromptStore";
52-
import { useTaskSessionStore } from "@/features/tasks/stores/taskSessionStore";
54+
import {
55+
getTaskSession,
56+
useTaskSessionStore,
57+
} from "@/features/tasks/stores/taskSessionStore";
5358
import { useTaskStore } from "@/features/tasks/stores/taskStore";
5459
import type { Task } from "@/features/tasks/types";
5560
import {
@@ -70,6 +75,7 @@ import { logger } from "@/lib/logger";
7075
import { useThemeColors } from "@/lib/theme";
7176

7277
const log = logger.scope("task-detail");
78+
type QueuedMessage = CloudTaskQueuedMessage<PendingAttachment>;
7379

7480
function getFirstParam(value?: string | string[]): string | undefined {
7581
return Array.isArray(value) ? value[0] : value;
@@ -108,12 +114,13 @@ export default function TaskDetailScreen() {
108114
sendInterrupting,
109115
sendPermissionResponse,
110116
setConfigOption,
111-
getSessionForTask,
112-
setFocusedTaskId,
113117
steerQueuedMessage,
114118
flushQueuedMessagesIfIdle,
115119
stopRun,
116-
} = useTaskSessionStore();
120+
} = taskSessionActions;
121+
const setFocusedTaskId = useTaskSessionStore(
122+
(state) => state.setFocusedTaskId,
123+
);
117124

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

131-
const session = taskId ? getSessionForTask(taskId) : undefined;
138+
const session = taskId ? getTaskSession(taskId) : undefined;
132139

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

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

@@ -368,11 +373,13 @@ export default function TaskDetailScreen() {
368373
// Saving an in-place edit: overwrite the queued message and release the
369374
// drain hold. If the turn already ended while editing, flush now — the
370375
// turn-end drain won't fire again on its own.
371-
const queue = useMessageQueueStore.getState();
372-
const editingId = queue.editingByTaskId[taskId];
376+
const editingId = taskMessageQueue.getSnapshot().editingByTaskId[taskId];
373377
if (editingId) {
374-
queue.update(taskId, editingId, { content: text, attachments });
375-
queue.clearEditing(taskId);
378+
taskMessageQueue.update(taskId, editingId, {
379+
content: text,
380+
attachments,
381+
});
382+
taskMessageQueue.clearEditing(taskId);
376383
flushQueuedMessagesIfIdle(taskId);
377384
return;
378385
}
@@ -394,7 +401,7 @@ export default function TaskDetailScreen() {
394401
// Steer interrupts the turn and resends right away.
395402
if (session?.isPromptPending) {
396403
if (messagingMode === "queue") {
397-
useMessageQueueStore.getState().enqueue(taskId, text, attachments);
404+
taskMessageQueue.enqueue(taskId, text, attachments);
398405
return;
399406
}
400407
sendInterrupting(taskId, text, attachments)
@@ -447,7 +454,7 @@ export default function TaskDetailScreen() {
447454
const handleEditQueued = useCallback(
448455
(message: QueuedMessage) => {
449456
if (!taskId) return;
450-
useMessageQueueStore.getState().setEditing(taskId, message.id);
457+
taskMessageQueue.setEditing(taskId, message.id);
451458
setRestoredDraft({
452459
text: message.content,
453460
attachments: message.attachments,
@@ -458,16 +465,16 @@ export default function TaskDetailScreen() {
458465

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

466473
const handleMoveQueued = useCallback(
467-
(message: QueuedMessage, direction: MoveDirection) => {
474+
(message: QueuedMessage, direction: CloudTaskQueueMoveDirection) => {
468475
if (!taskId) return;
469476
Haptics.selectionAsync();
470-
useMessageQueueStore.getState().move(taskId, message.id, direction);
477+
taskMessageQueue.move(taskId, message.id, direction);
471478
},
472479
[taskId],
473480
);
@@ -476,8 +483,8 @@ export default function TaskDetailScreen() {
476483
(message: QueuedMessage) => {
477484
if (!taskId) return;
478485
const wasEditing =
479-
useMessageQueueStore.getState().editingByTaskId[taskId] === message.id;
480-
useMessageQueueStore.getState().remove(taskId, message.id);
486+
taskMessageQueue.getSnapshot().editingByTaskId[taskId] === message.id;
487+
taskMessageQueue.remove(taskId, message.id);
481488
if (wasEditing) setRestoredDraft({ text: "", attachments: [] });
482489
},
483490
[taskId],
@@ -524,7 +531,7 @@ export default function TaskDetailScreen() {
524531
const handleStopRun = useCallback(() => {
525532
if (!taskId) return;
526533
confirmStopRun(() => {
527-
const promptsSent = countUserMessages(getSessionForTask(taskId)?.events);
534+
const promptsSent = countUserMessages(getTaskSession(taskId)?.events);
528535
stopRun(taskId)
529536
.then((ok) => {
530537
if (ok) {
@@ -542,7 +549,7 @@ export default function TaskDetailScreen() {
542549
})
543550
.catch(() => {});
544551
});
545-
}, [taskId, stopRun, analytics, getSessionForTask]);
552+
}, [taskId, stopRun, analytics]);
546553

547554
const canStopRun =
548555
!!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
@@ -18,10 +18,7 @@ export {
1818
useUpdateTask,
1919
} from "./hooks/useTasks";
2020
// Stores
21-
export {
22-
type TaskSession,
23-
useTaskSessionStore,
24-
} from "./stores/taskSessionStore";
21+
export { useTaskSessionStore } from "./stores/taskSessionStore";
2522
export { useTaskStore } from "./stores/taskStore";
2623

2724
// 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)