Add session-scoped routes for follow-up and queue operations (Vibe Kanban) (#1606)
* Yup, I'll hold off and do nothing for now. Let me know when you're ready to proceed with the session routes task, and I'll start exploring the codebase and planning the implementation. All cherry-pick conflicts resolved: 1. **`sessions/queue.rs`** - Fixed analytics to use `session.id` and `session.workspace_id` (2 locations) 2. **`task_attempts.rs`** - Removed the old `follow_up()` function that was already moved to `sessions/mod.rs` No remaining conflict markers in the routes directory. All fixes complete: 1. **`sessions/queue.rs`** ✓ - Fixed analytics to use `session.id` and `session.workspace_id` 2. **`task_attempts.rs`** ✓ - Removed old `follow_up` function (moved to sessions) 3. **`sessions/mod.rs`** ✓ - Fixed to use `find_latest_coding_agent_turn_session_id(pool, session.id)` instead of non-existent method The session-based `follow_up` now correctly scopes the agent_session_id lookup to the current session rather than across all workspace sessions. * All done: 1. ✓ `container.rs` - Changed to `find_latest_coding_agent_turn_session_id(pool, ctx.session.id)` 2. ✓ `pr.rs` - Changed to `find_latest_coding_agent_turn_session_id(pool, session.id)` 3. ✓ Deleted `find_latest_agent_session_id_by_workspace` from `execution_process.rs` All workspace-scoped `agent_session_id` lookups have been converted to session-scoped lookups and the unused method has been removed. Cleanup script changes for task attempt afc116fb-e639-4150-a299-b05c1f14f61e Done. Removed the unnecessary `Session::create` call and now using `&ctx.session` directly. Lint, db * Done. Simplified the validation: - Removed the extra `Session::find_by_id` DB call - Now directly checks `process.session_id != session.id` - Updated error message to "Process does not belong to this session"
This commit is contained in:
@@ -126,11 +126,11 @@ export function NextActionCard({
|
||||
}, [attemptId, task]);
|
||||
|
||||
const handleRunSetup = useCallback(async () => {
|
||||
if (!attemptId || !attempt) return;
|
||||
if (!attemptId || !attempt?.session?.executor) return;
|
||||
try {
|
||||
await attemptsApi.runAgentSetup(attemptId, {
|
||||
executor_profile_id: {
|
||||
executor: attempt.executor as BaseCodingAgent,
|
||||
executor: attempt.session.executor as BaseCodingAgent,
|
||||
variant: null,
|
||||
},
|
||||
});
|
||||
@@ -140,12 +140,14 @@ export function NextActionCard({
|
||||
}, [attemptId, attempt]);
|
||||
|
||||
const canAutoSetup = !!(
|
||||
attempt?.executor &&
|
||||
capabilities?.[attempt.executor]?.includes(BaseAgentCapability.SETUP_HELPER)
|
||||
attempt?.session?.executor &&
|
||||
capabilities?.[attempt.session.executor]?.includes(
|
||||
BaseAgentCapability.SETUP_HELPER
|
||||
)
|
||||
);
|
||||
|
||||
const setupHelpText = canAutoSetup
|
||||
? t('attempt.setupHelpText', { agent: attempt?.executor })
|
||||
? t('attempt.setupHelpText', { agent: attempt?.session?.executor })
|
||||
: null;
|
||||
|
||||
const editorName = getIdeName(config?.editor?.editor_type);
|
||||
|
||||
@@ -37,6 +37,9 @@ export function RetryEditorInline({
|
||||
const [message, setMessage] = useState(initialContent);
|
||||
const [sendError, setSendError] = useState<string | null>(null);
|
||||
|
||||
// Get sessionId from attempt's session
|
||||
const sessionId = attempt.session?.id;
|
||||
|
||||
// Extract variant from the process being retried
|
||||
const processVariant = useMemo<string | null>(() => {
|
||||
const process = attemptData.processes?.find(
|
||||
@@ -71,13 +74,13 @@ export function RetryEditorInline({
|
||||
});
|
||||
|
||||
const retryMutation = useRetryProcess(
|
||||
attemptId,
|
||||
sessionId ?? '',
|
||||
() => onCancelled?.(),
|
||||
(err) => setSendError((err as Error)?.message || 'Failed to send retry')
|
||||
);
|
||||
|
||||
const isSending = retryMutation.isPending;
|
||||
const canSend = !isAttemptRunning && !!message.trim();
|
||||
const canSend = !isAttemptRunning && !!message.trim() && !!sessionId;
|
||||
|
||||
const onCancel = () => {
|
||||
onCancelled?.();
|
||||
@@ -170,7 +173,7 @@ export function RetryEditorInline({
|
||||
<VariantSelector
|
||||
selectedVariant={selectedVariant}
|
||||
onChange={setSelectedVariant}
|
||||
currentProfile={profiles?.[attempt.executor] ?? null}
|
||||
currentProfile={profiles?.[attempt.session?.executor ?? ''] ?? null}
|
||||
/>
|
||||
<input
|
||||
ref={fileInputRef}
|
||||
|
||||
@@ -23,8 +23,8 @@ const UserMessage = ({
|
||||
const { isAttemptRunning } = useAttemptExecution(taskAttempt?.id);
|
||||
|
||||
const canFork = !!(
|
||||
taskAttempt?.executor &&
|
||||
capabilities?.[taskAttempt.executor]?.includes(
|
||||
taskAttempt?.session?.executor &&
|
||||
capabilities?.[taskAttempt.session.executor]?.includes(
|
||||
BaseAgentCapability.SESSION_FORK
|
||||
)
|
||||
);
|
||||
|
||||
@@ -99,8 +99,8 @@ const CreateAttemptDialogImpl = NiceModal.create<CreateAttemptDialogProps>(
|
||||
}, [modal.visible, resetBranchSelection]);
|
||||
|
||||
const defaultProfile: ExecutorProfileId | null = useMemo(() => {
|
||||
if (latestAttempt?.executor) {
|
||||
const lastExec = latestAttempt.executor as BaseCodingAgent;
|
||||
if (latestAttempt?.session?.executor) {
|
||||
const lastExec = latestAttempt.session.executor as BaseCodingAgent;
|
||||
// If the last attempt used the same executor as the user's current preference,
|
||||
// we assume they want to use their preferred variant as well.
|
||||
// Otherwise, we default to the "default" variant (null) since we don't know
|
||||
@@ -116,7 +116,7 @@ const CreateAttemptDialogImpl = NiceModal.create<CreateAttemptDialogProps>(
|
||||
};
|
||||
}
|
||||
return config?.executor_profile ?? null;
|
||||
}, [latestAttempt?.executor, config?.executor_profile]);
|
||||
}, [latestAttempt?.session?.executor, config?.executor_profile]);
|
||||
|
||||
const effectiveProfile = userSelectedProfile ?? defaultProfile;
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ const TaskAttemptPanel = ({
|
||||
<VirtualizedList key={attempt.id} attempt={attempt} task={task} />
|
||||
),
|
||||
followUp: (
|
||||
<TaskFollowUpSection task={task} selectedAttemptId={attempt.id} />
|
||||
<TaskFollowUpSection task={task} session={attempt.session} />
|
||||
),
|
||||
})}
|
||||
</RetryUiProvider>
|
||||
|
||||
@@ -80,7 +80,7 @@ const TaskPanel = ({ task }: TaskPanelProps) => {
|
||||
{
|
||||
id: 'executor',
|
||||
header: '',
|
||||
accessor: (attempt) => attempt.executor || 'Base Agent',
|
||||
accessor: (attempt) => attempt.session?.executor || 'Base Agent',
|
||||
className: 'pr-4',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -60,24 +60,30 @@ import { useQueueStatus } from '@/hooks/useQueueStatus';
|
||||
import { imagesApi, attemptsApi } from '@/lib/api';
|
||||
import { GitHubCommentsDialog } from '@/components/dialogs/tasks/GitHubCommentsDialog';
|
||||
import type { NormalizedComment } from '@/components/ui/wysiwyg/nodes/github-comment-node';
|
||||
import type { Session } from 'shared/types';
|
||||
|
||||
interface TaskFollowUpSectionProps {
|
||||
task: TaskWithAttemptStatus;
|
||||
selectedAttemptId?: string;
|
||||
session?: Session;
|
||||
}
|
||||
|
||||
export function TaskFollowUpSection({
|
||||
task,
|
||||
selectedAttemptId,
|
||||
session,
|
||||
}: TaskFollowUpSectionProps) {
|
||||
const { t } = useTranslation('tasks');
|
||||
const { projectId } = useProject();
|
||||
|
||||
// Derive IDs from session
|
||||
const workspaceId = session?.workspace_id;
|
||||
const sessionId = session?.id;
|
||||
|
||||
const { isAttemptRunning, stopExecution, isStopping, processes } =
|
||||
useAttemptExecution(selectedAttemptId, task.id);
|
||||
useAttemptExecution(workspaceId, task.id);
|
||||
|
||||
const { data: branchStatus, refetch: refetchBranchStatus } =
|
||||
useBranchStatus(selectedAttemptId);
|
||||
const { repos, selectedRepoId } = useAttemptRepo(selectedAttemptId);
|
||||
useBranchStatus(workspaceId);
|
||||
const { repos, selectedRepoId } = useAttemptRepo(workspaceId);
|
||||
|
||||
const getSelectedRepoId = useCallback(() => {
|
||||
return selectedRepoId ?? repos[0]?.id;
|
||||
@@ -91,7 +97,7 @@ export function TaskFollowUpSection({
|
||||
[branchStatus]
|
||||
);
|
||||
const { branch: attemptBranch, refetch: refetchAttemptBranch } =
|
||||
useAttemptBranch(selectedAttemptId);
|
||||
useAttemptBranch(workspaceId);
|
||||
const { profiles } = useUserSystem();
|
||||
const { comments, generateReviewMarkdown, clearComments } = useReview();
|
||||
const {
|
||||
@@ -127,7 +133,7 @@ export function TaskFollowUpSection({
|
||||
scratch,
|
||||
updateScratch,
|
||||
isLoading: isScratchLoading,
|
||||
} = useScratch(ScratchType.DRAFT_FOLLOW_UP, selectedAttemptId ?? '');
|
||||
} = useScratch(ScratchType.DRAFT_FOLLOW_UP, workspaceId ?? '');
|
||||
|
||||
// Derive the message and variant from scratch
|
||||
const scratchData: DraftFollowUpData | undefined =
|
||||
@@ -201,7 +207,7 @@ export function TaskFollowUpSection({
|
||||
// Uses scratchRef to avoid callback invalidation when scratch updates
|
||||
const saveToScratch = useCallback(
|
||||
async (message: string, variant: string | null) => {
|
||||
if (!selectedAttemptId) return;
|
||||
if (!workspaceId) return;
|
||||
// Don't create empty scratch entries - only save if there's actual content,
|
||||
// a variant is selected, or scratch already exists (to allow clearing a draft)
|
||||
if (!message.trim() && !variant && !scratchRef.current) return;
|
||||
@@ -216,7 +222,7 @@ export function TaskFollowUpSection({
|
||||
console.error('Failed to save follow-up draft', e);
|
||||
}
|
||||
},
|
||||
[selectedAttemptId, updateScratch]
|
||||
[workspaceId, updateScratch]
|
||||
);
|
||||
|
||||
// Wrapper to update variant and save to scratch immediately
|
||||
@@ -259,7 +265,7 @@ export function TaskFollowUpSection({
|
||||
queueMessage,
|
||||
cancelQueue,
|
||||
refresh: refreshQueueStatus,
|
||||
} = useQueueStatus(selectedAttemptId);
|
||||
} = useQueueStatus(sessionId);
|
||||
|
||||
// Track previous process count to detect new processes
|
||||
const prevProcessCountRef = useRef(processes.length);
|
||||
@@ -269,7 +275,7 @@ export function TaskFollowUpSection({
|
||||
const prevCount = prevProcessCountRef.current;
|
||||
prevProcessCountRef.current = processes.length;
|
||||
|
||||
if (!selectedAttemptId) return;
|
||||
if (!workspaceId) return;
|
||||
|
||||
// Refresh when execution stops
|
||||
if (!isAttemptRunning) {
|
||||
@@ -286,7 +292,7 @@ export function TaskFollowUpSection({
|
||||
}
|
||||
}, [
|
||||
isAttemptRunning,
|
||||
selectedAttemptId,
|
||||
workspaceId,
|
||||
processes.length,
|
||||
refreshQueueStatus,
|
||||
scratchData?.message,
|
||||
@@ -312,7 +318,7 @@ export function TaskFollowUpSection({
|
||||
// Send follow-up action
|
||||
const { isSendingFollowUp, followUpError, setFollowUpError, onSendFollowUp } =
|
||||
useFollowUpSend({
|
||||
attemptId: selectedAttemptId,
|
||||
sessionId,
|
||||
message: localMessage,
|
||||
conflictMarkdown: conflictResolutionInstructions,
|
||||
reviewMarkdown,
|
||||
@@ -329,7 +335,7 @@ export function TaskFollowUpSection({
|
||||
|
||||
// Separate logic for when textarea should be disabled vs when send button should be disabled
|
||||
const canTypeFollowUp = useMemo(() => {
|
||||
if (!selectedAttemptId || processes.length === 0 || isSendingFollowUp) {
|
||||
if (!workspaceId || processes.length === 0 || isSendingFollowUp) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -338,7 +344,7 @@ export function TaskFollowUpSection({
|
||||
// Note: isQueued no longer blocks typing - editing auto-cancels the queue
|
||||
return true;
|
||||
}, [
|
||||
selectedAttemptId,
|
||||
workspaceId,
|
||||
processes.length,
|
||||
isSendingFollowUp,
|
||||
isRetryActive,
|
||||
@@ -369,22 +375,22 @@ export function TaskFollowUpSection({
|
||||
const hasAnyScript = true;
|
||||
|
||||
const handleRunSetupScript = useCallback(async () => {
|
||||
if (!selectedAttemptId || isAttemptRunning) return;
|
||||
if (!workspaceId || isAttemptRunning) return;
|
||||
try {
|
||||
await attemptsApi.runSetupScript(selectedAttemptId);
|
||||
await attemptsApi.runSetupScript(workspaceId);
|
||||
} catch (error) {
|
||||
console.error('Failed to run setup script:', error);
|
||||
}
|
||||
}, [selectedAttemptId, isAttemptRunning]);
|
||||
}, [workspaceId, isAttemptRunning]);
|
||||
|
||||
const handleRunCleanupScript = useCallback(async () => {
|
||||
if (!selectedAttemptId || isAttemptRunning) return;
|
||||
if (!workspaceId || isAttemptRunning) return;
|
||||
try {
|
||||
await attemptsApi.runCleanupScript(selectedAttemptId);
|
||||
await attemptsApi.runCleanupScript(workspaceId);
|
||||
} catch (error) {
|
||||
console.error('Failed to run cleanup script:', error);
|
||||
}
|
||||
}, [selectedAttemptId, isAttemptRunning]);
|
||||
}, [workspaceId, isAttemptRunning]);
|
||||
|
||||
// Handler to queue the current message for execution after agent finishes
|
||||
const handleQueueMessage = useCallback(async () => {
|
||||
@@ -469,14 +475,11 @@ export function TaskFollowUpSection({
|
||||
// Handle image paste - upload to container and insert markdown
|
||||
const handlePasteFiles = useCallback(
|
||||
async (files: File[]) => {
|
||||
if (!selectedAttemptId) return;
|
||||
if (!workspaceId) return;
|
||||
|
||||
for (const file of files) {
|
||||
try {
|
||||
const response = await imagesApi.uploadForAttempt(
|
||||
selectedAttemptId,
|
||||
file
|
||||
);
|
||||
const response = await imagesApi.uploadForAttempt(workspaceId, file);
|
||||
// Append markdown image to current message
|
||||
const imageMarkdown = ``;
|
||||
|
||||
@@ -503,7 +506,7 @@ export function TaskFollowUpSection({
|
||||
}
|
||||
}
|
||||
},
|
||||
[selectedAttemptId]
|
||||
[workspaceId]
|
||||
);
|
||||
|
||||
// Attachment button - file input ref and handlers
|
||||
@@ -527,12 +530,12 @@ export function TaskFollowUpSection({
|
||||
|
||||
// Handler for GitHub comments insertion
|
||||
const handleGitHubCommentClick = useCallback(async () => {
|
||||
if (!selectedAttemptId) return;
|
||||
if (!workspaceId) return;
|
||||
const repoId = getSelectedRepoId();
|
||||
if (!repoId) return;
|
||||
|
||||
const result = await GitHubCommentsDialog.show({
|
||||
attemptId: selectedAttemptId,
|
||||
attemptId: workspaceId,
|
||||
repoId,
|
||||
});
|
||||
if (result.comments.length > 0) {
|
||||
@@ -575,7 +578,7 @@ export function TaskFollowUpSection({
|
||||
});
|
||||
}
|
||||
}
|
||||
}, [selectedAttemptId, getSelectedRepoId]);
|
||||
}, [workspaceId, getSelectedRepoId]);
|
||||
|
||||
// Stable onChange handler for WYSIWYGEditor
|
||||
const handleEditorChange = useCallback(
|
||||
@@ -637,19 +640,19 @@ export function TaskFollowUpSection({
|
||||
// When a process completes (e.g., agent resolved conflicts), refresh branch status promptly
|
||||
const prevRunningRef = useRef<boolean>(isAttemptRunning);
|
||||
useEffect(() => {
|
||||
if (prevRunningRef.current && !isAttemptRunning && selectedAttemptId) {
|
||||
if (prevRunningRef.current && !isAttemptRunning && workspaceId) {
|
||||
refetchBranchStatus();
|
||||
refetchAttemptBranch();
|
||||
}
|
||||
prevRunningRef.current = isAttemptRunning;
|
||||
}, [
|
||||
isAttemptRunning,
|
||||
selectedAttemptId,
|
||||
workspaceId,
|
||||
refetchBranchStatus,
|
||||
refetchAttemptBranch,
|
||||
]);
|
||||
|
||||
if (!selectedAttemptId) return null;
|
||||
if (!workspaceId) return null;
|
||||
|
||||
if (isScratchLoading) {
|
||||
return (
|
||||
@@ -688,7 +691,7 @@ export function TaskFollowUpSection({
|
||||
{/* Conflict notice and actions (optional UI) */}
|
||||
{branchStatus && (
|
||||
<FollowUpConflictSection
|
||||
selectedAttemptId={selectedAttemptId}
|
||||
workspaceId={workspaceId}
|
||||
attemptBranch={attemptBranch}
|
||||
branchStatus={branchStatus}
|
||||
isEditable={isEditable}
|
||||
@@ -734,7 +737,7 @@ export function TaskFollowUpSection({
|
||||
disabled={!isEditable}
|
||||
onPasteFiles={handlePasteFiles}
|
||||
projectId={projectId}
|
||||
taskAttemptId={selectedAttemptId}
|
||||
taskAttemptId={workspaceId}
|
||||
onCmdEnter={handleSubmitShortcut}
|
||||
className="min-h-[40px]"
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useAttemptConflicts } from '@/hooks/useAttemptConflicts';
|
||||
import type { RepoBranchStatus } from 'shared/types';
|
||||
|
||||
type Props = {
|
||||
selectedAttemptId?: string;
|
||||
workspaceId?: string;
|
||||
attemptBranch: string | null;
|
||||
branchStatus: RepoBranchStatus[] | undefined;
|
||||
isEditable: boolean;
|
||||
@@ -16,7 +16,7 @@ type Props = {
|
||||
};
|
||||
|
||||
export function FollowUpConflictSection({
|
||||
selectedAttemptId,
|
||||
workspaceId,
|
||||
attemptBranch,
|
||||
branchStatus,
|
||||
onResolve,
|
||||
@@ -28,9 +28,9 @@ export function FollowUpConflictSection({
|
||||
(r) => r.is_rebase_in_progress || (r.conflicted_files?.length ?? 0) > 0
|
||||
);
|
||||
const op = repoWithConflicts?.conflict_op ?? null;
|
||||
const openInEditor = useOpenInEditor(selectedAttemptId);
|
||||
const openInEditor = useOpenInEditor(workspaceId);
|
||||
const repoId = repoWithConflicts?.repo_id;
|
||||
const { abortConflicts } = useAttemptConflicts(selectedAttemptId, repoId);
|
||||
const { abortConflicts } = useAttemptConflicts(workspaceId, repoId);
|
||||
|
||||
// write using setAborting and read through abortingRef in async handlers
|
||||
const [aborting, setAborting] = useState(false);
|
||||
@@ -51,12 +51,12 @@ export function FollowUpConflictSection({
|
||||
onResolve={onResolve}
|
||||
enableResolve={enableResolve && !aborting}
|
||||
onOpenEditor={() => {
|
||||
if (!selectedAttemptId) return;
|
||||
if (!workspaceId) return;
|
||||
const first = repoWithConflicts.conflicted_files?.[0];
|
||||
openInEditor(first ? { filePath: first } : undefined);
|
||||
}}
|
||||
onAbort={async () => {
|
||||
if (!selectedAttemptId) return;
|
||||
if (!workspaceId) return;
|
||||
if (!enableAbort || abortingRef.current) return;
|
||||
try {
|
||||
setAborting(true);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import { attemptsApi } from '@/lib/api';
|
||||
import { sessionsApi } from '@/lib/api';
|
||||
import type { CreateFollowUpAttempt } from 'shared/types';
|
||||
|
||||
type Args = {
|
||||
attemptId?: string;
|
||||
sessionId?: string;
|
||||
message: string;
|
||||
conflictMarkdown: string | null;
|
||||
reviewMarkdown: string;
|
||||
@@ -15,7 +15,7 @@ type Args = {
|
||||
};
|
||||
|
||||
export function useFollowUpSend({
|
||||
attemptId,
|
||||
sessionId,
|
||||
message,
|
||||
conflictMarkdown,
|
||||
reviewMarkdown,
|
||||
@@ -29,7 +29,7 @@ export function useFollowUpSend({
|
||||
const [followUpError, setFollowUpError] = useState<string | null>(null);
|
||||
|
||||
const onSendFollowUp = useCallback(async () => {
|
||||
if (!attemptId) return;
|
||||
if (!sessionId) return;
|
||||
const extraMessage = message.trim();
|
||||
const finalPrompt = [
|
||||
conflictMarkdown,
|
||||
@@ -50,7 +50,7 @@ export function useFollowUpSend({
|
||||
force_when_dirty: null,
|
||||
perform_git_reset: null,
|
||||
};
|
||||
await attemptsApi.followUp(attemptId, body);
|
||||
await sessionsApi.followUp(sessionId, body);
|
||||
clearComments();
|
||||
clearClickedElements?.();
|
||||
onAfterSendCleanup();
|
||||
@@ -64,7 +64,7 @@ export function useFollowUpSend({
|
||||
setIsSendingFollowUp(false);
|
||||
}
|
||||
}, [
|
||||
attemptId,
|
||||
sessionId,
|
||||
message,
|
||||
conflictMarkdown,
|
||||
reviewMarkdown,
|
||||
|
||||
@@ -19,55 +19,55 @@ interface UseQueueStatusResult {
|
||||
refresh: () => Promise<void>;
|
||||
}
|
||||
|
||||
export function useQueueStatus(attemptId?: string): UseQueueStatusResult {
|
||||
export function useQueueStatus(sessionId?: string): UseQueueStatusResult {
|
||||
const [queueStatus, setQueueStatus] = useState<QueueStatus>({
|
||||
status: 'empty',
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
if (!attemptId) return;
|
||||
if (!sessionId) return;
|
||||
try {
|
||||
const status = await queueApi.getStatus(attemptId);
|
||||
const status = await queueApi.getStatus(sessionId);
|
||||
setQueueStatus(status);
|
||||
} catch (e) {
|
||||
console.error('Failed to fetch queue status:', e);
|
||||
}
|
||||
}, [attemptId]);
|
||||
}, [sessionId]);
|
||||
|
||||
const queueMessage = useCallback(
|
||||
async (message: string, variant: string | null) => {
|
||||
if (!attemptId) return;
|
||||
if (!sessionId) return;
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const status = await queueApi.queue(attemptId, { message, variant });
|
||||
const status = await queueApi.queue(sessionId, { message, variant });
|
||||
setQueueStatus(status);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[attemptId]
|
||||
[sessionId]
|
||||
);
|
||||
|
||||
const cancelQueue = useCallback(async () => {
|
||||
if (!attemptId) return;
|
||||
if (!sessionId) return;
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const status = await queueApi.cancel(attemptId);
|
||||
const status = await queueApi.cancel(sessionId);
|
||||
setQueueStatus(status);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [attemptId]);
|
||||
}, [sessionId]);
|
||||
|
||||
// Fetch initial status when attemptId changes
|
||||
// Fetch initial status when sessionId changes
|
||||
useEffect(() => {
|
||||
if (attemptId) {
|
||||
if (sessionId) {
|
||||
refresh();
|
||||
} else {
|
||||
setQueueStatus({ status: 'empty' });
|
||||
}
|
||||
}, [attemptId, refresh]);
|
||||
}, [sessionId, refresh]);
|
||||
|
||||
const isQueued = queueStatus.status === 'queued';
|
||||
const queuedMessage = isQueued
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useMutation } from '@tanstack/react-query';
|
||||
import { attemptsApi } from '@/lib/api';
|
||||
import { sessionsApi } from '@/lib/api';
|
||||
import {
|
||||
RestoreLogsDialog,
|
||||
type RestoreLogsDialogResult,
|
||||
@@ -22,7 +22,7 @@ class RetryDialogCancelledError extends Error {
|
||||
}
|
||||
|
||||
export function useRetryProcess(
|
||||
attemptId: string,
|
||||
sessionId: string,
|
||||
onSuccess?: () => void,
|
||||
onError?: (err: unknown) => void
|
||||
) {
|
||||
@@ -50,7 +50,7 @@ export function useRetryProcess(
|
||||
}
|
||||
|
||||
// Send the retry request
|
||||
await attemptsApi.followUp(attemptId, {
|
||||
await sessionsApi.followUp(sessionId, {
|
||||
prompt: message,
|
||||
variant,
|
||||
retry_process_id: executionProcessId,
|
||||
|
||||
@@ -29,7 +29,7 @@ export function useTaskAttempts(taskId?: string, opts?: Options) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook for components that need executor field for all attempts.
|
||||
* Hook for components that need session data for all attempts.
|
||||
* Fetches all attempts and their sessions in parallel.
|
||||
*/
|
||||
export function useTaskAttemptsWithSessions(taskId?: string, opts?: Options) {
|
||||
@@ -45,8 +45,8 @@ export function useTaskAttemptsWithSessions(taskId?: string, opts?: Options) {
|
||||
attempts.map((attempt) => sessionsApi.getByWorkspace(attempt.id))
|
||||
);
|
||||
return attempts.map((attempt, i) => {
|
||||
const executor = sessionsResults[i][0]?.executor ?? 'unknown';
|
||||
return createWorkspaceWithSession(attempt, executor);
|
||||
const session = sessionsResults[i][0];
|
||||
return createWorkspaceWithSession(attempt, session);
|
||||
});
|
||||
},
|
||||
enabled,
|
||||
|
||||
@@ -475,6 +475,33 @@ export const sessionsApi = {
|
||||
);
|
||||
return handleApiResponse<Session[]>(response);
|
||||
},
|
||||
|
||||
getById: async (sessionId: string): Promise<Session> => {
|
||||
const response = await makeRequest(`/api/sessions/${sessionId}`);
|
||||
return handleApiResponse<Session>(response);
|
||||
},
|
||||
|
||||
create: async (data: {
|
||||
workspace_id: string;
|
||||
executor?: string;
|
||||
}): Promise<Session> => {
|
||||
const response = await makeRequest('/api/sessions', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return handleApiResponse<Session>(response);
|
||||
},
|
||||
|
||||
followUp: async (
|
||||
sessionId: string,
|
||||
data: CreateFollowUpAttempt
|
||||
): Promise<ExecutionProcess> => {
|
||||
const response = await makeRequest(`/api/sessions/${sessionId}/follow-up`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return handleApiResponse<ExecutionProcess>(response);
|
||||
},
|
||||
};
|
||||
|
||||
// Task Attempts APIs
|
||||
@@ -496,14 +523,13 @@ export const attemptsApi = {
|
||||
return handleApiResponse<Workspace>(response);
|
||||
},
|
||||
|
||||
/** Get workspace with executor from latest session (for components that need executor) */
|
||||
/** Get workspace with latest session */
|
||||
getWithSession: async (attemptId: string): Promise<WorkspaceWithSession> => {
|
||||
const [workspace, sessions] = await Promise.all([
|
||||
attemptsApi.get(attemptId),
|
||||
sessionsApi.getByWorkspace(attemptId),
|
||||
]);
|
||||
const executor = sessions[0]?.executor ?? 'unknown';
|
||||
return createWorkspaceWithSession(workspace, executor);
|
||||
return createWorkspaceWithSession(workspace, sessions[0]);
|
||||
},
|
||||
|
||||
create: async (data: CreateTaskAttemptBody): Promise<Workspace> => {
|
||||
@@ -521,20 +547,6 @@ export const attemptsApi = {
|
||||
return handleApiResponse<void>(response);
|
||||
},
|
||||
|
||||
followUp: async (
|
||||
attemptId: string,
|
||||
data: CreateFollowUpAttempt
|
||||
): Promise<void> => {
|
||||
const response = await makeRequest(
|
||||
`/api/task-attempts/${attemptId}/follow-up`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
}
|
||||
);
|
||||
return handleApiResponse<void>(response);
|
||||
},
|
||||
|
||||
runAgentSetup: async (
|
||||
attemptId: string,
|
||||
data: RunAgentSetupRequest
|
||||
@@ -1244,43 +1256,37 @@ export const scratchApi = {
|
||||
`/api/scratch/${scratchType}/${id}/stream/ws`,
|
||||
};
|
||||
|
||||
// Queue API for task attempt follow-up messages
|
||||
// Queue API for session follow-up messages
|
||||
export const queueApi = {
|
||||
/**
|
||||
* Queue a follow-up message to be executed when current execution finishes
|
||||
*/
|
||||
queue: async (
|
||||
attemptId: string,
|
||||
sessionId: string,
|
||||
data: { message: string; variant: string | null }
|
||||
): Promise<QueueStatus> => {
|
||||
const response = await makeRequest(
|
||||
`/api/task-attempts/${attemptId}/queue`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
}
|
||||
);
|
||||
const response = await makeRequest(`/api/sessions/${sessionId}/queue`, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data),
|
||||
});
|
||||
return handleApiResponse<QueueStatus>(response);
|
||||
},
|
||||
|
||||
/**
|
||||
* Cancel a queued follow-up message
|
||||
*/
|
||||
cancel: async (attemptId: string): Promise<QueueStatus> => {
|
||||
const response = await makeRequest(
|
||||
`/api/task-attempts/${attemptId}/queue`,
|
||||
{
|
||||
method: 'DELETE',
|
||||
}
|
||||
);
|
||||
cancel: async (sessionId: string): Promise<QueueStatus> => {
|
||||
const response = await makeRequest(`/api/sessions/${sessionId}/queue`, {
|
||||
method: 'DELETE',
|
||||
});
|
||||
return handleApiResponse<QueueStatus>(response);
|
||||
},
|
||||
|
||||
/**
|
||||
* Get the current queue status for a task attempt
|
||||
* Get the current queue status for a session
|
||||
*/
|
||||
getStatus: async (attemptId: string): Promise<QueueStatus> => {
|
||||
const response = await makeRequest(`/api/task-attempts/${attemptId}/queue`);
|
||||
getStatus: async (sessionId: string): Promise<QueueStatus> => {
|
||||
const response = await makeRequest(`/api/sessions/${sessionId}/queue`);
|
||||
return handleApiResponse<QueueStatus>(response);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import type { Workspace } from 'shared/types';
|
||||
import type { Workspace, Session } from 'shared/types';
|
||||
|
||||
/**
|
||||
* WorkspaceWithSession includes executor from the latest Session.
|
||||
* Only used by components that actually need the executor field.
|
||||
* WorkspaceWithSession includes the latest Session for the workspace.
|
||||
* Provides access to session.id, session.executor, etc.
|
||||
*/
|
||||
export type WorkspaceWithSession = Workspace & {
|
||||
executor: string;
|
||||
session: Session | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a WorkspaceWithSession from a Workspace and executor string.
|
||||
* Create a WorkspaceWithSession from a Workspace and Session.
|
||||
*/
|
||||
export function createWorkspaceWithSession(
|
||||
workspace: Workspace,
|
||||
executor: string
|
||||
session: Session | undefined
|
||||
): WorkspaceWithSession {
|
||||
return {
|
||||
...workspace,
|
||||
executor,
|
||||
session,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user