Files
openchamber/packages/ui/src/hooks/useSessionActivity.ts
T

76 lines
3.0 KiB
TypeScript
Raw Normal View History

2025-12-07 19:32:53 +02:00
import React from 'react';
import { useSessionUIStore } from '@/sync/session-ui-store';
import { useSessionStatus, useSessionMessages, useSessionPermissions, useSessionQuestions } from '@/sync/sync-context';
2025-12-07 19:32:53 +02:00
// Mirrors OpenCode SessionStatus: busy|retry|idle.
type SessionActivityPhase = 'idle' | 'busy' | 'retry';
2025-12-07 19:32:53 +02:00
export interface SessionActivityResult {
phase: SessionActivityPhase;
isWorking: boolean;
isBusy: boolean;
isCooldown: boolean;
}
const IDLE_RESULT: SessionActivityResult = {
phase: 'idle',
isWorking: false,
isBusy: false,
isCooldown: false,
};
/**
* Determines if a session is actively working.
* Checks session_status and, only when status is missing, falls back to the
* trailing assistant message when its completion update has not landed yet.
* Returns idle when permissions or questions are pending (the permission /
* question indicator takes priority, and the send button must stay available so
* the user can supersede the prompt with a new message).
*/
function useSessionActivity(sessionId: string | null | undefined, directory?: string): SessionActivityResult {
const status = useSessionStatus(sessionId ?? '', directory);
const messages = useSessionMessages(sessionId ?? '', directory);
const permissions = useSessionPermissions(sessionId ?? '', directory);
const questions = useSessionQuestions(sessionId ?? '', directory);
2025-12-07 19:32:53 +02:00
return React.useMemo<SessionActivityResult>(() => {
if (!sessionId) return IDLE_RESULT;
// Permissions or questions pending → idle (the blocking indicator takes
// priority and the send button must remain a send, not a stop).
if (permissions.length > 0 || questions.length > 0) return IDLE_RESULT;
const phase: SessionActivityPhase = (status?.type ?? 'idle') as SessionActivityPhase;
// Only trust the trailing assistant message as a transient fallback while
// waiting for session.status/message.updated to settle.
const lastMessage = messages[messages.length - 1];
const hasPendingAssistant = Boolean(
lastMessage
&& lastMessage.role === 'assistant'
&& typeof (lastMessage as { time?: { completed?: number } }).time?.completed !== 'number',
);
const hasAuthoritativeStatus = status !== undefined;
const statusWorking = hasAuthoritativeStatus && phase !== 'idle';
const isWorking = statusWorking || hasPendingAssistant;
if (hasAuthoritativeStatus && !statusWorking) return IDLE_RESULT;
if (!isWorking) return IDLE_RESULT;
2025-12-07 19:32:53 +02:00
return {
phase: statusWorking ? phase : 'busy',
isWorking: true,
isBusy: phase === 'busy' || (!statusWorking && hasPendingAssistant),
isCooldown: false,
2025-12-07 19:32:53 +02:00
};
}, [sessionId, status, messages, permissions, questions]);
2025-12-07 19:32:53 +02:00
}
export function useCurrentSessionActivity(): SessionActivityResult {
const currentSessionId = useSessionUIStore((state) => state.currentSessionId);
const currentSessionDirectory = useSessionUIStore((state) => state.currentSessionDirectory);
return useSessionActivity(currentSessionId, currentSessionDirectory ?? undefined);
2025-12-07 19:32:53 +02:00
}