diff --git a/src/components/cloud-agent-next/CloudAgentProvider.tsx b/src/components/cloud-agent-next/CloudAgentProvider.tsx index ffede7dc2..38ea81721 100644 --- a/src/components/cloud-agent-next/CloudAgentProvider.tsx +++ b/src/components/cloud-agent-next/CloudAgentProvider.tsx @@ -13,6 +13,7 @@ import { type CloudAgentSessionId, } from '@/lib/cloud-agent-sdk'; import { SESSION_INGEST_WS_URL } from '@/lib/constants'; +import { usePostHog } from 'posthog-js/react'; const ManagerContext = createContext(null); @@ -24,6 +25,9 @@ type CloudAgentProviderProps = { export function CloudAgentProvider({ children, organizationId }: CloudAgentProviderProps) { const storeRef = useRef(createStore()); const trpcClient = useRawTRPCClient(); + const posthog = usePostHog(); + const posthogRef = useRef(posthog); + posthogRef.current = posthog; // Create manager once per provider instance. // trpcClient is stable (from context); organizationId is stable per provider mount. @@ -259,6 +263,19 @@ export function CloudAgentProvider({ children, organizationId }: CloudAgentProvi window.history.replaceState(window.history.state, '', url.toString()); } }, + + onRemoteSessionOpened: ({ kiloSessionId }) => { + posthogRef.current?.capture('remote_session_opened', { + feature: 'remote-session', + kilo_session_id: kiloSessionId, + }); + }, + onRemoteSessionMessageSent: ({ kiloSessionId }) => { + posthogRef.current?.capture('remote_session_message_sent', { + feature: 'remote-session', + kilo_session_id: kiloSessionId, + }); + }, }); } diff --git a/src/lib/cloud-agent-sdk/session-manager.ts b/src/lib/cloud-agent-sdk/session-manager.ts index d2c732d22..307da0b40 100644 --- a/src/lib/cloud-agent-sdk/session-manager.ts +++ b/src/lib/cloud-agent-sdk/session-manager.ts @@ -40,6 +40,7 @@ type SessionConfig = { model: string; variant?: string | null; }; +type ActiveSessionType = 'cloud-agent' | 'cli'; type StandaloneQuestion = { requestId: string; questions: QuestionInfo[] }; type StandalonePermission = { requestId: string; @@ -95,6 +96,8 @@ type SessionManagerConfig = { onComplete?: () => void; onBranchChanged?: (branch: string) => void; onSendFailed?: (messageText: string) => void; + onRemoteSessionOpened?: (data: { kiloSessionId: KiloSessionId }) => void; + onRemoteSessionMessageSent?: (data: { kiloSessionId: KiloSessionId }) => void; }; // Writable/read-only atom aliases for the public atoms record @@ -299,6 +302,7 @@ function createSessionManager(config: SessionManagerConfig): SessionManager { // Private mutable state let activeSessionId: KiloSessionId | null = null; let currentSession: CloudAgentSession | null = null; + let activeSessionType: ActiveSessionType | null = null; let stateUnsub: (() => void) | null = null; let indicatorTimer: ReturnType | null = null; @@ -418,6 +422,7 @@ function createSessionManager(config: SessionManagerConfig): SessionManager { async function switchSession(kiloSessionId: KiloSessionId): Promise { activeSessionId = kiloSessionId; + activeSessionType = null; stateUnsub?.(); stateUnsub = null; currentSession?.destroy(); @@ -500,6 +505,10 @@ function createSessionManager(config: SessionManagerConfig): SessionManager { const ap = store.get(activePermissionAtom); if (ap?.requestId === requestId) store.set(activePermissionAtom, null); }, + onResolved: resolved => { + if (resolved.cloudAgentSessionId) activeSessionType = 'cloud-agent'; + else if (resolved.isLive) activeSessionType = 'cli'; + }, onBranchChanged: branch => { const currentFetched = store.get(fetchedSessionDataAtom); if (currentFetched) { @@ -538,6 +547,9 @@ function createSessionManager(config: SessionManagerConfig): SessionManager { // Fallback: clear loading when events flow even if no root // session.created was replayed (e.g. CLI snapshot failure). store.set(isLoadingAtom, false); + if (activeSessionType === 'cli') { + config.onRemoteSessionOpened?.({ kiloSessionId }); + } }, }); session.connect(); @@ -567,12 +579,18 @@ function createSessionManager(config: SessionManagerConfig): SessionManager { } satisfies StoredMessage); try { if (!currentSession) throw new Error('No active session'); + // Snapshot before await — switchSession() can overwrite these while send is in flight. + const sessionType = activeSessionType; + const kiloSessionId = activeSessionId; await currentSession.send({ prompt: payload.prompt, mode: payload.mode, model: payload.model, variant: payload.variant, }); + if (sessionType === 'cli' && kiloSessionId) { + config.onRemoteSessionMessageSent?.({ kiloSessionId }); + } } catch (err) { store.set(optimisticMessageAtom, null); store.set(failedPromptAtom, payload.prompt); @@ -635,6 +653,7 @@ function createSessionManager(config: SessionManagerConfig): SessionManager { } clearAllAtoms(); activeSessionId = null; + activeSessionType = null; } return { diff --git a/src/lib/cloud-agent-sdk/session.ts b/src/lib/cloud-agent-sdk/session.ts index afa8a5b9c..aa89e7b75 100644 --- a/src/lib/cloud-agent-sdk/session.ts +++ b/src/lib/cloud-agent-sdk/session.ts @@ -42,6 +42,7 @@ type CloudAgentSessionConfig = { ) => void; onPermissionResolved?: (requestId: string) => void; onBranchChanged?: (branch: string) => void; + onResolved?: (resolved: ResolvedSession) => void; onSessionCreated?: (info: SessionInfo) => void; onSessionUpdated?: (info: SessionInfo) => void; onEvent?: (event: NormalizedEvent) => void; @@ -225,6 +226,7 @@ function createCloudAgentSession(config: CloudAgentSessionConfig): CloudAgentSes if (expectedGeneration !== connectGeneration) return; console.log('[cli-debug] resolveAndConnect: resolved=%o', resolved); + config.onResolved?.(resolved); let factory: TransportFactory; try {