feat (acp): custom methods for managing scheduler (#9972)
This commit is contained in:
@@ -0,0 +1,193 @@
|
||||
import type {
|
||||
CreateScheduleRequest_unstable,
|
||||
InspectRunningJobResponse_unstable,
|
||||
KillRunningJobResponse_unstable,
|
||||
RunScheduleNowResponse_unstable,
|
||||
ScheduledJobDto,
|
||||
SessionInfo,
|
||||
} from '@aaif/goose-sdk';
|
||||
import { getAcpClient } from './acpConnection';
|
||||
|
||||
let inFlightListSchedules: Promise<ScheduledJobDto[]> | null = null;
|
||||
const inFlightListScheduleSessions = new Map<string, Promise<SessionInfo[]>>();
|
||||
|
||||
function acpErrorMessage(error: unknown): string | null {
|
||||
if (typeof error !== 'object' || error === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const candidate = 'error' in error && isRecord(error.error) ? error.error : error;
|
||||
if (!isRecord(candidate)) {
|
||||
return null;
|
||||
}
|
||||
if (typeof candidate.data === 'string') {
|
||||
return candidate.data;
|
||||
}
|
||||
return typeof candidate.message === 'string' ? candidate.message : null;
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null;
|
||||
}
|
||||
|
||||
function normalizeAcpError(error: unknown, fallback: string): Error {
|
||||
const message = acpErrorMessage(error);
|
||||
if (message) {
|
||||
return new Error(message);
|
||||
}
|
||||
if (error instanceof Error) {
|
||||
return error;
|
||||
}
|
||||
return new Error(fallback);
|
||||
}
|
||||
|
||||
function clearInFlightScheduleReads(): void {
|
||||
inFlightListSchedules = null;
|
||||
inFlightListScheduleSessions.clear();
|
||||
}
|
||||
|
||||
export async function acpListSchedules(): Promise<ScheduledJobDto[]> {
|
||||
const pending = inFlightListSchedules;
|
||||
if (pending) {
|
||||
return pending;
|
||||
}
|
||||
|
||||
const listPromise = (async () => {
|
||||
const client = await getAcpClient();
|
||||
const response = await client.goose.schedulesList_unstable({});
|
||||
return response.jobs;
|
||||
})().catch((error) => {
|
||||
throw normalizeAcpError(error, 'Failed to list schedules');
|
||||
});
|
||||
|
||||
inFlightListSchedules = listPromise;
|
||||
|
||||
try {
|
||||
return await listPromise;
|
||||
} finally {
|
||||
if (inFlightListSchedules === listPromise) {
|
||||
inFlightListSchedules = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function acpCreateSchedule(
|
||||
request: CreateScheduleRequest_unstable
|
||||
): Promise<ScheduledJobDto> {
|
||||
try {
|
||||
const client = await getAcpClient();
|
||||
const response = await client.goose.schedulesCreate_unstable(request);
|
||||
clearInFlightScheduleReads();
|
||||
return response.job;
|
||||
} catch (error) {
|
||||
throw normalizeAcpError(error, 'Failed to create schedule');
|
||||
}
|
||||
}
|
||||
|
||||
export async function acpDeleteSchedule(scheduleId: string): Promise<void> {
|
||||
try {
|
||||
const client = await getAcpClient();
|
||||
await client.goose.schedulesDelete_unstable({ scheduleId });
|
||||
clearInFlightScheduleReads();
|
||||
} catch (error) {
|
||||
throw normalizeAcpError(error, 'Failed to delete schedule');
|
||||
}
|
||||
}
|
||||
|
||||
export async function acpListScheduleSessions(
|
||||
scheduleId: string,
|
||||
limit: number
|
||||
): Promise<SessionInfo[]> {
|
||||
const key = `${scheduleId}:${limit}`;
|
||||
const pending = inFlightListScheduleSessions.get(key);
|
||||
if (pending) {
|
||||
return pending;
|
||||
}
|
||||
|
||||
const listPromise = (async () => {
|
||||
const client = await getAcpClient();
|
||||
const response = await client.goose.schedulesSessionsList_unstable({ scheduleId, limit });
|
||||
return response.sessions;
|
||||
})().catch((error) => {
|
||||
throw normalizeAcpError(error, 'Failed to list schedule sessions');
|
||||
});
|
||||
|
||||
inFlightListScheduleSessions.set(key, listPromise);
|
||||
|
||||
try {
|
||||
return await listPromise;
|
||||
} finally {
|
||||
if (inFlightListScheduleSessions.get(key) === listPromise) {
|
||||
inFlightListScheduleSessions.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function acpRunScheduleNow(
|
||||
scheduleId: string
|
||||
): Promise<RunScheduleNowResponse_unstable> {
|
||||
try {
|
||||
const client = await getAcpClient();
|
||||
const response = await client.goose.schedulesRunNow_unstable({ scheduleId });
|
||||
clearInFlightScheduleReads();
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw normalizeAcpError(error, 'Failed to run schedule now');
|
||||
}
|
||||
}
|
||||
|
||||
export async function acpPauseSchedule(scheduleId: string): Promise<void> {
|
||||
try {
|
||||
const client = await getAcpClient();
|
||||
await client.goose.schedulesPause_unstable({ scheduleId });
|
||||
clearInFlightScheduleReads();
|
||||
} catch (error) {
|
||||
throw normalizeAcpError(error, 'Failed to pause schedule');
|
||||
}
|
||||
}
|
||||
|
||||
export async function acpUnpauseSchedule(scheduleId: string): Promise<void> {
|
||||
try {
|
||||
const client = await getAcpClient();
|
||||
await client.goose.schedulesUnpause_unstable({ scheduleId });
|
||||
clearInFlightScheduleReads();
|
||||
} catch (error) {
|
||||
throw normalizeAcpError(error, 'Failed to unpause schedule');
|
||||
}
|
||||
}
|
||||
|
||||
export async function acpUpdateSchedule(
|
||||
scheduleId: string,
|
||||
cron: string
|
||||
): Promise<ScheduledJobDto> {
|
||||
try {
|
||||
const client = await getAcpClient();
|
||||
const response = await client.goose.schedulesUpdate_unstable({ scheduleId, cron });
|
||||
clearInFlightScheduleReads();
|
||||
return response.job;
|
||||
} catch (error) {
|
||||
throw normalizeAcpError(error, 'Failed to update schedule');
|
||||
}
|
||||
}
|
||||
|
||||
export async function acpKillRunningJob(jobId: string): Promise<KillRunningJobResponse_unstable> {
|
||||
try {
|
||||
const client = await getAcpClient();
|
||||
const response = await client.goose.schedulesRunningJobKill_unstable({ jobId });
|
||||
clearInFlightScheduleReads();
|
||||
return response;
|
||||
} catch (error) {
|
||||
throw normalizeAcpError(error, 'Failed to kill running job');
|
||||
}
|
||||
}
|
||||
|
||||
export async function acpInspectRunningJob(
|
||||
jobId: string
|
||||
): Promise<InspectRunningJobResponse_unstable> {
|
||||
try {
|
||||
const client = await getAcpClient();
|
||||
return await client.goose.schedulesRunningJobInspect_unstable({ jobId });
|
||||
} catch (error) {
|
||||
throw normalizeAcpError(error, 'Failed to inspect running job');
|
||||
}
|
||||
}
|
||||
@@ -991,8 +991,8 @@ export default function RecipesView() {
|
||||
id: scheduleRecipeManifest.id,
|
||||
source: '',
|
||||
cron: scheduleRecipeManifest.schedule_cron,
|
||||
last_run: null,
|
||||
currently_running: false,
|
||||
lastRun: null,
|
||||
currentlyRunning: false,
|
||||
paused: false,
|
||||
}
|
||||
: null
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ScheduledJob } from '../../schedule';
|
||||
import type { ScheduledJobDto } from '@aaif/goose-sdk';
|
||||
import { errorMessage } from '../../utils/conversionUtils';
|
||||
import { defineMessages, useIntl } from '../../i18n';
|
||||
import {
|
||||
@@ -61,7 +61,7 @@ const i18n = defineMessages({
|
||||
});
|
||||
|
||||
interface CronPickerProps {
|
||||
schedule: ScheduledJob | null;
|
||||
schedule: ScheduledJobDto | null;
|
||||
onChange: (cron: string) => void;
|
||||
isValid: (valid: boolean) => void;
|
||||
}
|
||||
|
||||
@@ -1,29 +1,28 @@
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import type { ScheduledJobDto, SessionInfo } from '@aaif/goose-sdk';
|
||||
import { Button } from '../ui/button';
|
||||
import { ScrollArea } from '../ui/scroll-area';
|
||||
import BackButton from '../ui/BackButton';
|
||||
import { Card } from '../ui/card';
|
||||
import {
|
||||
getScheduleSessions,
|
||||
runScheduleNow,
|
||||
pauseSchedule,
|
||||
unpauseSchedule,
|
||||
updateSchedule,
|
||||
listSchedules,
|
||||
killRunningJob,
|
||||
inspectRunningJob,
|
||||
ScheduledJob,
|
||||
} from '../../schedule';
|
||||
import SessionHistoryView from '../sessions/SessionHistoryView';
|
||||
acpListScheduleSessions,
|
||||
acpRunScheduleNow,
|
||||
acpPauseSchedule,
|
||||
acpUnpauseSchedule,
|
||||
acpUpdateSchedule,
|
||||
acpListSchedules,
|
||||
acpKillRunningJob,
|
||||
acpInspectRunningJob,
|
||||
} from '../../acp/schedules';
|
||||
import { ScheduleModal, NewSchedulePayload } from './ScheduleModal';
|
||||
import { toastError, toastSuccess } from '../../toasts';
|
||||
import { Loader2, Pause, Play, Edit, Square, Eye } from 'lucide-react';
|
||||
import cronstrue from 'cronstrue';
|
||||
import { formatToLocalDateWithTimezone } from '../../utils/date';
|
||||
import { getSession, Session } from '../../api';
|
||||
import { trackScheduleRunNow, getErrorType } from '../../utils/analytics';
|
||||
import { errorMessage } from '../../utils/conversionUtils';
|
||||
import { defineMessages, useIntl } from '../../i18n';
|
||||
import { useNavigation } from '../../hooks/useNavigation';
|
||||
|
||||
const i18n = defineMessages({
|
||||
scheduleNotFound: { id: 'scheduleDetailView.scheduleNotFound', defaultMessage: 'Schedule Not Found' },
|
||||
@@ -57,12 +56,11 @@ const i18n = defineMessages({
|
||||
created: { id: 'scheduleDetailView.created', defaultMessage: 'Created: {date}' },
|
||||
messages: { id: 'scheduleDetailView.messages', defaultMessage: 'Messages: {count}' },
|
||||
dir: { id: 'scheduleDetailView.dir', defaultMessage: 'Dir: {path}' },
|
||||
tokens: { id: 'scheduleDetailView.tokens', defaultMessage: 'Tokens: {count}' },
|
||||
idLabel: { id: 'scheduleDetailView.idLabel', defaultMessage: 'ID:' },
|
||||
jobCancelled: { id: 'scheduleDetailView.jobCancelled', defaultMessage: 'Job Cancelled' },
|
||||
jobCancelledMsg: { id: 'scheduleDetailView.jobCancelledMsg', defaultMessage: 'The job was cancelled while starting up.' },
|
||||
scheduleTriggered: { id: 'scheduleDetailView.scheduleTriggered', defaultMessage: 'Schedule Triggered' },
|
||||
newSession: { id: 'scheduleDetailView.newSession', defaultMessage: 'New session: {sessionId}' },
|
||||
scheduleCompleted: { id: 'scheduleDetailView.scheduleCompleted', defaultMessage: 'Run completed' },
|
||||
completedSession: { id: 'scheduleDetailView.completedSession', defaultMessage: 'Session: {sessionId}' },
|
||||
runScheduleError: { id: 'scheduleDetailView.runScheduleError', defaultMessage: 'Run Schedule Error' },
|
||||
scheduleUnpaused: { id: 'scheduleDetailView.scheduleUnpaused', defaultMessage: 'Schedule Unpaused' },
|
||||
unpausedMsg: { id: 'scheduleDetailView.unpausedMsg', defaultMessage: 'Unpaused "{id}"' },
|
||||
@@ -77,53 +75,48 @@ const i18n = defineMessages({
|
||||
scheduleUpdated: { id: 'scheduleDetailView.scheduleUpdated', defaultMessage: 'Schedule Updated' },
|
||||
updatedMsg: { id: 'scheduleDetailView.updatedMsg', defaultMessage: 'Updated "{id}"' },
|
||||
updateScheduleError: { id: 'scheduleDetailView.updateScheduleError', defaultMessage: 'Update Schedule Error' },
|
||||
failedToLoadSession: { id: 'scheduleDetailView.failedToLoadSession', defaultMessage: 'Failed to load session' },
|
||||
scheduleNotFoundError: { id: 'scheduleDetailView.scheduleNotFoundError', defaultMessage: 'Schedule not found' },
|
||||
});
|
||||
|
||||
interface ScheduleSessionMeta {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: string;
|
||||
workingDir?: string;
|
||||
scheduleId?: string | null;
|
||||
messageCount?: number;
|
||||
totalTokens?: number | null;
|
||||
inputTokens?: number | null;
|
||||
outputTokens?: number | null;
|
||||
accumulatedTotalTokens?: number | null;
|
||||
accumulatedInputTokens?: number | null;
|
||||
accumulatedOutputTokens?: number | null;
|
||||
}
|
||||
|
||||
interface ScheduleDetailViewProps {
|
||||
scheduleId: string | null;
|
||||
onNavigateBack: () => void;
|
||||
}
|
||||
|
||||
function sessionMeta(session: SessionInfo): Record<string, unknown> {
|
||||
return typeof session._meta === 'object' && session._meta !== null ? session._meta : {};
|
||||
}
|
||||
|
||||
function metaString(session: SessionInfo, key: string): string | undefined {
|
||||
const value = sessionMeta(session)[key];
|
||||
return typeof value === 'string' ? value : undefined;
|
||||
}
|
||||
|
||||
function metaNumber(session: SessionInfo, key: string): number | undefined {
|
||||
const value = sessionMeta(session)[key];
|
||||
return typeof value === 'number' ? value : undefined;
|
||||
}
|
||||
|
||||
const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onNavigateBack }) => {
|
||||
const intl = useIntl();
|
||||
const [sessions, setSessions] = useState<ScheduleSessionMeta[]>([]);
|
||||
const setView = useNavigation();
|
||||
const [sessions, setSessions] = useState<SessionInfo[]>([]);
|
||||
const [isLoadingSessions, setIsLoadingSessions] = useState(false);
|
||||
const [sessionsError, setSessionsError] = useState<string | null>(null);
|
||||
|
||||
const [scheduleDetails, setScheduleDetails] = useState<ScheduledJob | null>(null);
|
||||
const [scheduleDetails, setScheduleDetails] = useState<ScheduledJobDto | null>(null);
|
||||
const [isLoadingSchedule, setIsLoadingSchedule] = useState(false);
|
||||
const [scheduleError, setScheduleError] = useState<string | null>(null);
|
||||
|
||||
const [isActionLoading, setIsActionLoading] = useState(false);
|
||||
|
||||
const [selectedSession, setSelectedSession] = useState<Session | null>(null);
|
||||
const [isLoadingSession, setIsLoadingSession] = useState(false);
|
||||
const [sessionError, setSessionError] = useState<string | null>(null);
|
||||
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
|
||||
const fetchSessions = async (sId: string) => {
|
||||
setIsLoadingSessions(true);
|
||||
setSessionsError(null);
|
||||
try {
|
||||
const data = await getScheduleSessions(sId, 20);
|
||||
const data = await acpListScheduleSessions(sId, 20);
|
||||
setSessions(data);
|
||||
} catch (err) {
|
||||
setSessionsError(errorMessage(err, 'Failed to fetch sessions'));
|
||||
@@ -136,7 +129,7 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
|
||||
setIsLoadingSchedule(true);
|
||||
setScheduleError(null);
|
||||
try {
|
||||
const allSchedules = await listSchedules();
|
||||
const allSchedules = await acpListSchedules();
|
||||
const schedule = allSchedules.find((s) => s.id === sId);
|
||||
if (schedule) {
|
||||
setScheduleDetails(schedule);
|
||||
@@ -151,22 +144,27 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
|
||||
}, [intl]);
|
||||
|
||||
useEffect(() => {
|
||||
if (scheduleId && !selectedSession) {
|
||||
if (scheduleId) {
|
||||
fetchSessions(scheduleId);
|
||||
fetchSchedule(scheduleId);
|
||||
}
|
||||
}, [scheduleId, selectedSession, fetchSchedule]);
|
||||
}, [scheduleId, fetchSchedule]);
|
||||
|
||||
const openSession = useCallback((sessionId: string) => {
|
||||
setView('pair', {
|
||||
disableAnimation: true,
|
||||
resumeSessionId: sessionId,
|
||||
});
|
||||
}, [setView]);
|
||||
|
||||
const handleRunNow = async () => {
|
||||
if (!scheduleId) return;
|
||||
setIsActionLoading(true);
|
||||
try {
|
||||
const newSessionId = await runScheduleNow(scheduleId);
|
||||
const result = await acpRunScheduleNow(scheduleId);
|
||||
trackScheduleRunNow(true);
|
||||
if (newSessionId === 'CANCELLED') {
|
||||
toastSuccess({ title: intl.formatMessage(i18n.jobCancelled), msg: intl.formatMessage(i18n.jobCancelledMsg) });
|
||||
} else {
|
||||
toastSuccess({ title: intl.formatMessage(i18n.scheduleTriggered), msg: intl.formatMessage(i18n.newSession, { sessionId: newSessionId }) });
|
||||
if (result.status === 'completed' && result.sessionId) {
|
||||
toastSuccess({ title: intl.formatMessage(i18n.scheduleCompleted), msg: intl.formatMessage(i18n.completedSession, { sessionId: result.sessionId }) });
|
||||
}
|
||||
await fetchSessions(scheduleId);
|
||||
await fetchSchedule(scheduleId);
|
||||
@@ -187,10 +185,10 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
|
||||
setIsActionLoading(true);
|
||||
try {
|
||||
if (scheduleDetails.paused) {
|
||||
await unpauseSchedule(scheduleId);
|
||||
await acpUnpauseSchedule(scheduleId);
|
||||
toastSuccess({ title: intl.formatMessage(i18n.scheduleUnpaused), msg: intl.formatMessage(i18n.unpausedMsg, { id: scheduleId }) });
|
||||
} else {
|
||||
await pauseSchedule(scheduleId);
|
||||
await acpPauseSchedule(scheduleId);
|
||||
toastSuccess({ title: intl.formatMessage(i18n.schedulePaused), msg: intl.formatMessage(i18n.pausedMsg, { id: scheduleId }) });
|
||||
}
|
||||
await fetchSchedule(scheduleId);
|
||||
@@ -209,7 +207,7 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
|
||||
if (!scheduleId) return;
|
||||
setIsActionLoading(true);
|
||||
try {
|
||||
const result = await killRunningJob(scheduleId);
|
||||
const result = await acpKillRunningJob(scheduleId);
|
||||
toastSuccess({ title: intl.formatMessage(i18n.jobKilled), msg: result.message });
|
||||
await fetchSchedule(scheduleId);
|
||||
} catch (err) {
|
||||
@@ -227,7 +225,7 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
|
||||
if (!scheduleId) return;
|
||||
setIsActionLoading(true);
|
||||
try {
|
||||
const result = await inspectRunningJob(scheduleId);
|
||||
const result = await acpInspectRunningJob(scheduleId);
|
||||
if (result.sessionId) {
|
||||
const duration = result.runningDurationSeconds
|
||||
? `${Math.floor(result.runningDurationSeconds / 60)}m ${result.runningDurationSeconds % 60}s`
|
||||
@@ -254,7 +252,7 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
|
||||
if (!scheduleId) return;
|
||||
setIsActionLoading(true);
|
||||
try {
|
||||
await updateSchedule(scheduleId, payload as string);
|
||||
await acpUpdateSchedule(scheduleId, payload as string);
|
||||
toastSuccess({ title: intl.formatMessage(i18n.scheduleUpdated), msg: intl.formatMessage(i18n.updatedMsg, { id: scheduleId }) });
|
||||
await fetchSchedule(scheduleId);
|
||||
setIsModalOpen(false);
|
||||
@@ -269,37 +267,6 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
|
||||
}
|
||||
};
|
||||
|
||||
const loadSession = async (sessionId: string) => {
|
||||
setIsLoadingSession(true);
|
||||
setSessionError(null);
|
||||
try {
|
||||
const response = await getSession<true>({
|
||||
path: { session_id: sessionId },
|
||||
throwOnError: true,
|
||||
});
|
||||
setSelectedSession(response.data);
|
||||
} catch (err) {
|
||||
const msg = errorMessage(err, 'Failed to load session');
|
||||
setSessionError(msg);
|
||||
toastError({ title: intl.formatMessage(i18n.failedToLoadSession), msg });
|
||||
} finally {
|
||||
setIsLoadingSession(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (selectedSession) {
|
||||
return (
|
||||
<SessionHistoryView
|
||||
session={selectedSession}
|
||||
isLoading={isLoadingSession}
|
||||
error={sessionError}
|
||||
onBack={() => setSelectedSession(null)}
|
||||
onRetry={() => loadSession(selectedSession.id)}
|
||||
showActionButtons={true}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!scheduleId) {
|
||||
return (
|
||||
<div className="h-screen w-full flex flex-col items-center justify-center bg-white dark:bg-gray-900 text-text-primary p-8">
|
||||
@@ -352,7 +319,7 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
|
||||
{scheduleDetails.id}
|
||||
</h3>
|
||||
<div className="mt-2 md:mt-0 flex items-center gap-2">
|
||||
{scheduleDetails.currently_running && (
|
||||
{scheduleDetails.currentlyRunning && (
|
||||
<div className="text-sm text-green-500 dark:text-green-400 font-semibold flex items-center">
|
||||
<span className="inline-block w-2 h-2 bg-green-500 dark:bg-green-400 rounded-full mr-1 animate-pulse"></span>
|
||||
{intl.formatMessage(i18n.currentlyRunning)}
|
||||
@@ -377,18 +344,18 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
|
||||
</p>
|
||||
<p className="text-sm text-text-primary">
|
||||
<span className="font-semibold">{intl.formatMessage(i18n.lastRun)}</span>{' '}
|
||||
{formatToLocalDateWithTimezone(scheduleDetails.last_run)}
|
||||
{formatToLocalDateWithTimezone(scheduleDetails.lastRun)}
|
||||
</p>
|
||||
{scheduleDetails.currently_running && scheduleDetails.current_session_id && (
|
||||
{scheduleDetails.currentlyRunning && scheduleDetails.currentSessionId && (
|
||||
<p className="text-sm text-text-primary">
|
||||
<span className="font-semibold">{intl.formatMessage(i18n.currentSession)}</span>{' '}
|
||||
{scheduleDetails.current_session_id}
|
||||
{scheduleDetails.currentSessionId}
|
||||
</p>
|
||||
)}
|
||||
{scheduleDetails.currently_running && scheduleDetails.process_start_time && (
|
||||
{scheduleDetails.currentlyRunning && scheduleDetails.jobStartTime && (
|
||||
<p className="text-sm text-text-primary">
|
||||
<span className="font-semibold">{intl.formatMessage(i18n.processStarted)}</span>{' '}
|
||||
{formatToLocalDateWithTimezone(scheduleDetails.process_start_time)}
|
||||
{formatToLocalDateWithTimezone(scheduleDetails.jobStartTime)}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
@@ -401,13 +368,13 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
|
||||
<div className="flex flex-col md:flex-row gap-2">
|
||||
<Button
|
||||
onClick={handleRunNow}
|
||||
disabled={isActionLoading || scheduleDetails?.currently_running}
|
||||
disabled={isActionLoading || scheduleDetails?.currentlyRunning}
|
||||
className="w-full md:w-auto"
|
||||
>
|
||||
{intl.formatMessage(i18n.runScheduleNow)}
|
||||
</Button>
|
||||
|
||||
{scheduleDetails && !scheduleDetails.currently_running && (
|
||||
{scheduleDetails && !scheduleDetails.currentlyRunning && (
|
||||
<>
|
||||
<Button
|
||||
onClick={() => setIsModalOpen(true)}
|
||||
@@ -443,7 +410,7 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
|
||||
</>
|
||||
)}
|
||||
|
||||
{scheduleDetails?.currently_running && (
|
||||
{scheduleDetails?.currentlyRunning && (
|
||||
<>
|
||||
<Button
|
||||
onClick={handleInspect}
|
||||
@@ -467,7 +434,7 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
|
||||
)}
|
||||
</div>
|
||||
|
||||
{scheduleDetails?.currently_running && (
|
||||
{scheduleDetails?.currentlyRunning && (
|
||||
<p className="text-sm text-amber-600 dark:text-amber-400 mt-2">
|
||||
{intl.formatMessage(i18n.cannotModifyRunning)}
|
||||
</p>
|
||||
@@ -496,45 +463,46 @@ const ScheduleDetailView: React.FC<ScheduleDetailViewProps> = ({ scheduleId, onN
|
||||
|
||||
{sessions.length > 0 && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{sessions.map((session) => (
|
||||
<Card
|
||||
key={session.id}
|
||||
className="p-4 bg-background-primary shadow cursor-pointer hover:shadow-lg transition-shadow duration-200"
|
||||
onClick={() => loadSession(session.id)}
|
||||
>
|
||||
<h3
|
||||
className="text-sm font-semibold text-text-primary truncate"
|
||||
title={session.name || session.id}
|
||||
{sessions.map((session) => {
|
||||
const sessionId = String(session.sessionId);
|
||||
const sessionName = session.title ?? sessionId;
|
||||
const createdAt = metaString(session, 'createdAt') ?? session.updatedAt;
|
||||
const messageCount = metaNumber(session, 'messageCount');
|
||||
|
||||
return (
|
||||
<Card
|
||||
key={sessionId}
|
||||
className="p-4 bg-background-primary shadow cursor-pointer hover:shadow-lg transition-shadow duration-200"
|
||||
onClick={() => openSession(sessionId)}
|
||||
>
|
||||
{session.name || intl.formatMessage(i18n.sessionId, { id: session.id })}
|
||||
</h3>
|
||||
<p className="text-xs text-text-secondary mt-1">
|
||||
{intl.formatMessage(i18n.created, { date: session.createdAt ? formatToLocalDateWithTimezone(session.createdAt) : 'N/A' })}
|
||||
</p>
|
||||
{session.messageCount !== undefined && (
|
||||
<p className="text-xs text-text-secondary mt-1">
|
||||
{intl.formatMessage(i18n.messages, { count: session.messageCount })}
|
||||
</p>
|
||||
)}
|
||||
{session.workingDir && (
|
||||
<p
|
||||
className="text-xs text-text-secondary mt-1 truncate"
|
||||
title={session.workingDir}
|
||||
<h3
|
||||
className="text-sm font-semibold text-text-primary truncate"
|
||||
title={sessionName}
|
||||
>
|
||||
{intl.formatMessage(i18n.dir, { path: session.workingDir })}
|
||||
{sessionName || intl.formatMessage(i18n.sessionId, { id: sessionId })}
|
||||
</h3>
|
||||
<p className="text-xs text-text-secondary mt-1">
|
||||
{intl.formatMessage(i18n.created, { date: createdAt ? formatToLocalDateWithTimezone(createdAt) : 'N/A' })}
|
||||
</p>
|
||||
)}
|
||||
{session.accumulatedTotalTokens !== undefined &&
|
||||
session.accumulatedTotalTokens !== null && (
|
||||
{messageCount !== undefined && (
|
||||
<p className="text-xs text-text-secondary mt-1">
|
||||
{intl.formatMessage(i18n.tokens, { count: session.accumulatedTotalTokens })}
|
||||
{intl.formatMessage(i18n.messages, { count: messageCount })}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs text-text-secondary mt-1">
|
||||
{intl.formatMessage(i18n.idLabel)} <span className="font-mono">{session.id}</span>
|
||||
</p>
|
||||
</Card>
|
||||
))}
|
||||
{session.cwd && (
|
||||
<p
|
||||
className="text-xs text-text-secondary mt-1 truncate"
|
||||
title={session.cwd}
|
||||
>
|
||||
{intl.formatMessage(i18n.dir, { path: session.cwd })}
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs text-text-secondary mt-1">
|
||||
{intl.formatMessage(i18n.idLabel)} <span className="font-mono">{sessionId}</span>
|
||||
</p>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React, { useState, useEffect, FormEvent, useCallback } from 'react';
|
||||
import type { ScheduledJobDto } from '@aaif/goose-sdk';
|
||||
import { Card } from '../ui/card';
|
||||
import { Button } from '../ui/button';
|
||||
import { Input } from '../ui/input';
|
||||
import { ScheduledJob } from '../../schedule';
|
||||
import { CronPicker } from './CronPicker';
|
||||
import { Recipe, parseDeeplink, parseRecipeFromFile } from '../../recipe';
|
||||
import { getStorageDirectory } from '../../recipe/recipe_management';
|
||||
@@ -47,7 +47,7 @@ interface ScheduleModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (payload: NewSchedulePayload | string) => Promise<void>;
|
||||
schedule: ScheduledJob | null;
|
||||
schedule: ScheduledJobDto | null;
|
||||
isLoadingExternally: boolean;
|
||||
apiErrorExternally: string | null;
|
||||
initialDeepLink: string | null;
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
import type { ScheduledJobDto } from '@aaif/goose-sdk';
|
||||
import {
|
||||
listSchedules,
|
||||
createSchedule,
|
||||
deleteSchedule,
|
||||
pauseSchedule,
|
||||
unpauseSchedule,
|
||||
updateSchedule,
|
||||
killRunningJob,
|
||||
inspectRunningJob,
|
||||
ScheduledJob,
|
||||
} from '../../schedule';
|
||||
acpListSchedules,
|
||||
acpCreateSchedule,
|
||||
acpDeleteSchedule,
|
||||
acpPauseSchedule,
|
||||
acpUnpauseSchedule,
|
||||
acpUpdateSchedule,
|
||||
acpKillRunningJob,
|
||||
acpInspectRunningJob,
|
||||
} from '../../acp/schedules';
|
||||
import { ScrollArea } from '../ui/scroll-area';
|
||||
import { Card } from '../ui/card';
|
||||
import { Button } from '../ui/button';
|
||||
@@ -67,9 +67,9 @@ interface SchedulesViewProps {
|
||||
}
|
||||
|
||||
const ScheduleCard: React.FC<{
|
||||
job: ScheduledJob;
|
||||
job: ScheduledJobDto;
|
||||
onNavigateToDetail: (id: string) => void;
|
||||
onEdit: (job: ScheduledJob) => void;
|
||||
onEdit: (job: ScheduledJobDto) => void;
|
||||
onPause: (id: string) => void;
|
||||
onUnpause: (id: string) => void;
|
||||
onKill: (id: string) => void;
|
||||
@@ -95,7 +95,7 @@ const ScheduleCard: React.FC<{
|
||||
readableCron = job.cron;
|
||||
}
|
||||
|
||||
const formattedLastRun = formatToLocalDateWithTimezone(job.last_run);
|
||||
const formattedLastRun = formatToLocalDateWithTimezone(job.lastRun);
|
||||
|
||||
return (
|
||||
<Card
|
||||
@@ -108,7 +108,7 @@ const ScheduleCard: React.FC<{
|
||||
<h3 className="text-base truncate max-w-[50vw]" title={job.id}>
|
||||
{job.id}
|
||||
</h3>
|
||||
{job.currently_running && (
|
||||
{job.currentlyRunning && (
|
||||
<span className="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300">
|
||||
<span className="inline-block w-2 h-2 bg-green-500 rounded-full mr-1 animate-pulse"></span>
|
||||
{intl.formatMessage(i18n.running)}
|
||||
@@ -130,7 +130,7 @@ const ScheduleCard: React.FC<{
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 shrink-0">
|
||||
{!job.currently_running && (
|
||||
{!job.currentlyRunning && (
|
||||
<>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
@@ -173,7 +173,7 @@ const ScheduleCard: React.FC<{
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
{job.currently_running && (
|
||||
{job.currentlyRunning && (
|
||||
<>
|
||||
<Button
|
||||
onClick={(e) => {
|
||||
@@ -224,13 +224,13 @@ const ScheduleCard: React.FC<{
|
||||
const SchedulesView: React.FC<SchedulesViewProps> = ({ onClose: _onClose }) => {
|
||||
const intl = useIntl();
|
||||
const location = useLocation();
|
||||
const [schedules, setSchedules] = useState<ScheduledJob[]>([]);
|
||||
const [schedules, setSchedules] = useState<ScheduledJobDto[]>([]);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [apiError, setApiError] = useState<string | null>(null);
|
||||
const [submitApiError, setSubmitApiError] = useState<string | null>(null);
|
||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||
const [editingSchedule, setEditingSchedule] = useState<ScheduledJob | null>(null);
|
||||
const [editingSchedule, setEditingSchedule] = useState<ScheduledJobDto | null>(null);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
const [pendingDeepLink, setPendingDeepLink] = useState<string | null>(null);
|
||||
const [actionsInProgress, setActionsInProgress] = useState<Set<string>>(new Set());
|
||||
@@ -240,7 +240,7 @@ const SchedulesView: React.FC<SchedulesViewProps> = ({ onClose: _onClose }) => {
|
||||
setIsLoading(true);
|
||||
setApiError(null);
|
||||
try {
|
||||
const fetchedSchedules = await listSchedules();
|
||||
const fetchedSchedules = await acpListSchedules();
|
||||
setSchedules(fetchedSchedules);
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch schedules:', error);
|
||||
@@ -289,14 +289,14 @@ const SchedulesView: React.FC<SchedulesViewProps> = ({ onClose: _onClose }) => {
|
||||
setSubmitApiError(null);
|
||||
try {
|
||||
if (editingSchedule) {
|
||||
await updateSchedule(editingSchedule.id, payload as string);
|
||||
await acpUpdateSchedule(editingSchedule.id, payload as string);
|
||||
toastSuccess({
|
||||
title: intl.formatMessage(i18n.scheduleUpdated),
|
||||
msg: intl.formatMessage(i18n.scheduleUpdatedMsg, { id: editingSchedule.id }),
|
||||
});
|
||||
} else {
|
||||
const newPayload = payload as NewSchedulePayload;
|
||||
await createSchedule(newPayload);
|
||||
await acpCreateSchedule(newPayload);
|
||||
const sourceType = pendingDeepLink ? 'deeplink' : 'file';
|
||||
trackScheduleCreated(sourceType, true);
|
||||
}
|
||||
@@ -325,7 +325,7 @@ const SchedulesView: React.FC<SchedulesViewProps> = ({ onClose: _onClose }) => {
|
||||
setApiError(null);
|
||||
|
||||
try {
|
||||
await deleteSchedule(id);
|
||||
await acpDeleteSchedule(id);
|
||||
trackScheduleDeleted(true);
|
||||
await fetchSchedules();
|
||||
} catch (error) {
|
||||
@@ -347,7 +347,7 @@ const SchedulesView: React.FC<SchedulesViewProps> = ({ onClose: _onClose }) => {
|
||||
setApiError(null);
|
||||
|
||||
try {
|
||||
await pauseSchedule(id);
|
||||
await acpPauseSchedule(id);
|
||||
toastSuccess({
|
||||
title: intl.formatMessage(i18n.schedulePaused),
|
||||
msg: intl.formatMessage(i18n.schedulePausedMsg, { id }),
|
||||
@@ -375,7 +375,7 @@ const SchedulesView: React.FC<SchedulesViewProps> = ({ onClose: _onClose }) => {
|
||||
setApiError(null);
|
||||
|
||||
try {
|
||||
await unpauseSchedule(id);
|
||||
await acpUnpauseSchedule(id);
|
||||
toastSuccess({
|
||||
title: intl.formatMessage(i18n.scheduleUnpaused),
|
||||
msg: intl.formatMessage(i18n.scheduleUnpausedMsg, { id }),
|
||||
@@ -403,7 +403,7 @@ const SchedulesView: React.FC<SchedulesViewProps> = ({ onClose: _onClose }) => {
|
||||
setApiError(null);
|
||||
|
||||
try {
|
||||
const result = await killRunningJob(id);
|
||||
const result = await acpKillRunningJob(id);
|
||||
toastSuccess({
|
||||
title: intl.formatMessage(i18n.jobKilled),
|
||||
msg: result.message,
|
||||
@@ -431,7 +431,7 @@ const SchedulesView: React.FC<SchedulesViewProps> = ({ onClose: _onClose }) => {
|
||||
setApiError(null);
|
||||
|
||||
try {
|
||||
const result = await inspectRunningJob(id);
|
||||
const result = await acpInspectRunningJob(id);
|
||||
if (result.sessionId) {
|
||||
const duration = result.runningDurationSeconds
|
||||
? `${Math.floor(result.runningDurationSeconds / 60)}m ${result.runningDurationSeconds % 60}s`
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { render, screen, type RenderOptions, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import type { ScheduledJobDto } from '@aaif/goose-sdk';
|
||||
import { IntlTestWrapper } from '../../../i18n/test-utils';
|
||||
import { CronPicker } from '../CronPicker';
|
||||
import type { ScheduledJob } from '../../../schedule';
|
||||
|
||||
const renderWithIntl = (ui: React.ReactElement, options?: RenderOptions) =>
|
||||
render(ui, { wrapper: IntlTestWrapper, ...options });
|
||||
@@ -13,10 +13,12 @@ const getLastCron = (onChange: ReturnType<typeof vi.fn>) => {
|
||||
return calls[calls.length - 1]?.[0];
|
||||
};
|
||||
|
||||
const scheduledJob = (cron: string): ScheduledJob => ({
|
||||
const scheduledJob = (cron: string): ScheduledJobDto => ({
|
||||
id: 'quarterly-report',
|
||||
source: 'dummy.yaml',
|
||||
cron,
|
||||
currentlyRunning: false,
|
||||
paused: false,
|
||||
});
|
||||
|
||||
describe('CronPicker', () => {
|
||||
|
||||
@@ -3296,6 +3296,9 @@
|
||||
"scheduleDetailView.cannotModifyRunning": {
|
||||
"defaultMessage": "Cannot trigger or modify a schedule while it's already running."
|
||||
},
|
||||
"scheduleDetailView.completedSession": {
|
||||
"defaultMessage": "Session: {sessionId}"
|
||||
},
|
||||
"scheduleDetailView.created": {
|
||||
"defaultMessage": "Created: {date}"
|
||||
},
|
||||
@@ -3317,9 +3320,6 @@
|
||||
"scheduleDetailView.errorPrefix": {
|
||||
"defaultMessage": "Error: {error}"
|
||||
},
|
||||
"scheduleDetailView.failedToLoadSession": {
|
||||
"defaultMessage": "Failed to load session"
|
||||
},
|
||||
"scheduleDetailView.idLabel": {
|
||||
"defaultMessage": "ID:"
|
||||
},
|
||||
@@ -3362,9 +3362,6 @@
|
||||
"scheduleDetailView.messages": {
|
||||
"defaultMessage": "Messages: {count}"
|
||||
},
|
||||
"scheduleDetailView.newSession": {
|
||||
"defaultMessage": "New session: {sessionId}"
|
||||
},
|
||||
"scheduleDetailView.noScheduleId": {
|
||||
"defaultMessage": "No schedule ID provided. Return to schedules list."
|
||||
},
|
||||
@@ -3401,6 +3398,9 @@
|
||||
"scheduleDetailView.runScheduleNow": {
|
||||
"defaultMessage": "Run Schedule Now"
|
||||
},
|
||||
"scheduleDetailView.scheduleCompleted": {
|
||||
"defaultMessage": "Run completed"
|
||||
},
|
||||
"scheduleDetailView.scheduleDetails": {
|
||||
"defaultMessage": "Schedule Details"
|
||||
},
|
||||
@@ -3419,9 +3419,6 @@
|
||||
"scheduleDetailView.schedulePaused": {
|
||||
"defaultMessage": "Schedule Paused"
|
||||
},
|
||||
"scheduleDetailView.scheduleTriggered": {
|
||||
"defaultMessage": "Schedule Triggered"
|
||||
},
|
||||
"scheduleDetailView.scheduleUnpaused": {
|
||||
"defaultMessage": "Schedule Unpaused"
|
||||
},
|
||||
@@ -3431,9 +3428,6 @@
|
||||
"scheduleDetailView.sessionId": {
|
||||
"defaultMessage": "Session ID: {id}"
|
||||
},
|
||||
"scheduleDetailView.tokens": {
|
||||
"defaultMessage": "Tokens: {count}"
|
||||
},
|
||||
"scheduleDetailView.unpauseSchedule": {
|
||||
"defaultMessage": "Unpause Schedule"
|
||||
},
|
||||
|
||||
@@ -3296,6 +3296,9 @@
|
||||
"scheduleDetailView.cannotModifyRunning": {
|
||||
"defaultMessage": "No se puede activar ni modificar una programación mientras se está ejecutando."
|
||||
},
|
||||
"scheduleDetailView.completedSession": {
|
||||
"defaultMessage": "Sesión: {sessionId}"
|
||||
},
|
||||
"scheduleDetailView.created": {
|
||||
"defaultMessage": "Creada: {date}"
|
||||
},
|
||||
@@ -3317,9 +3320,6 @@
|
||||
"scheduleDetailView.errorPrefix": {
|
||||
"defaultMessage": "Error: {error}"
|
||||
},
|
||||
"scheduleDetailView.failedToLoadSession": {
|
||||
"defaultMessage": "No se pudo cargar la sesión"
|
||||
},
|
||||
"scheduleDetailView.idLabel": {
|
||||
"defaultMessage": "ID:"
|
||||
},
|
||||
@@ -3362,9 +3362,6 @@
|
||||
"scheduleDetailView.messages": {
|
||||
"defaultMessage": "Mensajes: {count}"
|
||||
},
|
||||
"scheduleDetailView.newSession": {
|
||||
"defaultMessage": "Nueva sesión: {sessionId}"
|
||||
},
|
||||
"scheduleDetailView.noScheduleId": {
|
||||
"defaultMessage": "No se proporcionó ningún ID de programación. Vuelve a la lista de programaciones."
|
||||
},
|
||||
@@ -3401,6 +3398,9 @@
|
||||
"scheduleDetailView.runScheduleNow": {
|
||||
"defaultMessage": "Ejecutar programación ahora"
|
||||
},
|
||||
"scheduleDetailView.scheduleCompleted": {
|
||||
"defaultMessage": "Ejecución completada"
|
||||
},
|
||||
"scheduleDetailView.scheduleDetails": {
|
||||
"defaultMessage": "Detalles de la programación"
|
||||
},
|
||||
@@ -3419,9 +3419,6 @@
|
||||
"scheduleDetailView.schedulePaused": {
|
||||
"defaultMessage": "Programación en pausa"
|
||||
},
|
||||
"scheduleDetailView.scheduleTriggered": {
|
||||
"defaultMessage": "Programación activada"
|
||||
},
|
||||
"scheduleDetailView.scheduleUnpaused": {
|
||||
"defaultMessage": "Programación reanudada"
|
||||
},
|
||||
@@ -3431,9 +3428,6 @@
|
||||
"scheduleDetailView.sessionId": {
|
||||
"defaultMessage": "ID de sesión: {id}"
|
||||
},
|
||||
"scheduleDetailView.tokens": {
|
||||
"defaultMessage": "Tokens: {count}"
|
||||
},
|
||||
"scheduleDetailView.unpauseSchedule": {
|
||||
"defaultMessage": "Reanudar programación"
|
||||
},
|
||||
|
||||
@@ -3296,6 +3296,9 @@
|
||||
"scheduleDetailView.cannotModifyRunning": {
|
||||
"defaultMessage": "जब कोई शेड्यूल पहले से चल रहा हो तो उसे ट्रिगर या संशोधित नहीं किया जा सकता।"
|
||||
},
|
||||
"scheduleDetailView.completedSession": {
|
||||
"defaultMessage": "सत्र: {sessionId}"
|
||||
},
|
||||
"scheduleDetailView.created": {
|
||||
"defaultMessage": "बनाया गया: {date}"
|
||||
},
|
||||
@@ -3317,9 +3320,6 @@
|
||||
"scheduleDetailView.errorPrefix": {
|
||||
"defaultMessage": "त्रुटि: {error}"
|
||||
},
|
||||
"scheduleDetailView.failedToLoadSession": {
|
||||
"defaultMessage": "सत्र लोड करने में विफल"
|
||||
},
|
||||
"scheduleDetailView.idLabel": {
|
||||
"defaultMessage": "आईडी:"
|
||||
},
|
||||
@@ -3362,9 +3362,6 @@
|
||||
"scheduleDetailView.messages": {
|
||||
"defaultMessage": "संदेश: {count}"
|
||||
},
|
||||
"scheduleDetailView.newSession": {
|
||||
"defaultMessage": "नया सत्र: {sessionId}"
|
||||
},
|
||||
"scheduleDetailView.noScheduleId": {
|
||||
"defaultMessage": "कोई शेड्यूल आईडी प्रदान नहीं की गई. शेड्यूल सूची पर लौटें."
|
||||
},
|
||||
@@ -3401,6 +3398,9 @@
|
||||
"scheduleDetailView.runScheduleNow": {
|
||||
"defaultMessage": "अभी शेड्यूल चलाएँ"
|
||||
},
|
||||
"scheduleDetailView.scheduleCompleted": {
|
||||
"defaultMessage": "रन पूरा हुआ"
|
||||
},
|
||||
"scheduleDetailView.scheduleDetails": {
|
||||
"defaultMessage": "अनुसूची विवरण"
|
||||
},
|
||||
@@ -3419,9 +3419,6 @@
|
||||
"scheduleDetailView.schedulePaused": {
|
||||
"defaultMessage": "शेड्यूल रोका गया"
|
||||
},
|
||||
"scheduleDetailView.scheduleTriggered": {
|
||||
"defaultMessage": "शेड्यूल ट्रिगर हुआ"
|
||||
},
|
||||
"scheduleDetailView.scheduleUnpaused": {
|
||||
"defaultMessage": "शेड्यूल अनरोका"
|
||||
},
|
||||
@@ -3431,9 +3428,6 @@
|
||||
"scheduleDetailView.sessionId": {
|
||||
"defaultMessage": "सत्र आईडी: {id}"
|
||||
},
|
||||
"scheduleDetailView.tokens": {
|
||||
"defaultMessage": "टोकन: {count}"
|
||||
},
|
||||
"scheduleDetailView.unpauseSchedule": {
|
||||
"defaultMessage": "शेड्यूल को फिर से रोकें"
|
||||
},
|
||||
|
||||
@@ -3296,6 +3296,9 @@
|
||||
"scheduleDetailView.cannotModifyRunning": {
|
||||
"defaultMessage": "既に実行中のため、スケジュールを実行または変更できません。"
|
||||
},
|
||||
"scheduleDetailView.completedSession": {
|
||||
"defaultMessage": "セッション: {sessionId}"
|
||||
},
|
||||
"scheduleDetailView.created": {
|
||||
"defaultMessage": "作成: {date}"
|
||||
},
|
||||
@@ -3317,9 +3320,6 @@
|
||||
"scheduleDetailView.errorPrefix": {
|
||||
"defaultMessage": "エラー: {error}"
|
||||
},
|
||||
"scheduleDetailView.failedToLoadSession": {
|
||||
"defaultMessage": "セッションの読み込みに失敗しました"
|
||||
},
|
||||
"scheduleDetailView.idLabel": {
|
||||
"defaultMessage": "ID:"
|
||||
},
|
||||
@@ -3362,9 +3362,6 @@
|
||||
"scheduleDetailView.messages": {
|
||||
"defaultMessage": "メッセージ: {count}"
|
||||
},
|
||||
"scheduleDetailView.newSession": {
|
||||
"defaultMessage": "新しいセッション: {sessionId}"
|
||||
},
|
||||
"scheduleDetailView.noScheduleId": {
|
||||
"defaultMessage": "スケジュールIDが指定されていません。スケジュール一覧に戻ってください。"
|
||||
},
|
||||
@@ -3401,6 +3398,9 @@
|
||||
"scheduleDetailView.runScheduleNow": {
|
||||
"defaultMessage": "スケジュールを今すぐ実行"
|
||||
},
|
||||
"scheduleDetailView.scheduleCompleted": {
|
||||
"defaultMessage": "実行が完了しました"
|
||||
},
|
||||
"scheduleDetailView.scheduleDetails": {
|
||||
"defaultMessage": "スケジュール詳細"
|
||||
},
|
||||
@@ -3419,9 +3419,6 @@
|
||||
"scheduleDetailView.schedulePaused": {
|
||||
"defaultMessage": "スケジュールを一時停止しました"
|
||||
},
|
||||
"scheduleDetailView.scheduleTriggered": {
|
||||
"defaultMessage": "スケジュールを実行しました"
|
||||
},
|
||||
"scheduleDetailView.scheduleUnpaused": {
|
||||
"defaultMessage": "スケジュールを再開しました"
|
||||
},
|
||||
@@ -3431,9 +3428,6 @@
|
||||
"scheduleDetailView.sessionId": {
|
||||
"defaultMessage": "セッションID: {id}"
|
||||
},
|
||||
"scheduleDetailView.tokens": {
|
||||
"defaultMessage": "トークン: {count}"
|
||||
},
|
||||
"scheduleDetailView.unpauseSchedule": {
|
||||
"defaultMessage": "スケジュールを再開"
|
||||
},
|
||||
|
||||
@@ -3296,6 +3296,9 @@
|
||||
"scheduleDetailView.cannotModifyRunning": {
|
||||
"defaultMessage": "이미 실행 중인 일정은 트리거하거나 수정할 수 없습니다."
|
||||
},
|
||||
"scheduleDetailView.completedSession": {
|
||||
"defaultMessage": "세션: {sessionId}"
|
||||
},
|
||||
"scheduleDetailView.created": {
|
||||
"defaultMessage": "생성일: {date}"
|
||||
},
|
||||
@@ -3317,9 +3320,6 @@
|
||||
"scheduleDetailView.errorPrefix": {
|
||||
"defaultMessage": "오류: {error}"
|
||||
},
|
||||
"scheduleDetailView.failedToLoadSession": {
|
||||
"defaultMessage": "세션을 로드하지 못했습니다."
|
||||
},
|
||||
"scheduleDetailView.idLabel": {
|
||||
"defaultMessage": "ID:"
|
||||
},
|
||||
@@ -3362,9 +3362,6 @@
|
||||
"scheduleDetailView.messages": {
|
||||
"defaultMessage": "메시지: {count}"
|
||||
},
|
||||
"scheduleDetailView.newSession": {
|
||||
"defaultMessage": "새 세션: {sessionId}"
|
||||
},
|
||||
"scheduleDetailView.noScheduleId": {
|
||||
"defaultMessage": "제공된 일정 ID가 없습니다. 일정 목록으로 돌아갑니다."
|
||||
},
|
||||
@@ -3401,6 +3398,9 @@
|
||||
"scheduleDetailView.runScheduleNow": {
|
||||
"defaultMessage": "지금 일정 실행"
|
||||
},
|
||||
"scheduleDetailView.scheduleCompleted": {
|
||||
"defaultMessage": "실행 완료"
|
||||
},
|
||||
"scheduleDetailView.scheduleDetails": {
|
||||
"defaultMessage": "일정 세부정보"
|
||||
},
|
||||
@@ -3419,9 +3419,6 @@
|
||||
"scheduleDetailView.schedulePaused": {
|
||||
"defaultMessage": "일정이 일시 중지됨"
|
||||
},
|
||||
"scheduleDetailView.scheduleTriggered": {
|
||||
"defaultMessage": "일정이 실행됨"
|
||||
},
|
||||
"scheduleDetailView.scheduleUnpaused": {
|
||||
"defaultMessage": "일정 일시중지 해제됨"
|
||||
},
|
||||
@@ -3431,9 +3428,6 @@
|
||||
"scheduleDetailView.sessionId": {
|
||||
"defaultMessage": "세션 ID: {id}"
|
||||
},
|
||||
"scheduleDetailView.tokens": {
|
||||
"defaultMessage": "토큰: {count}"
|
||||
},
|
||||
"scheduleDetailView.unpauseSchedule": {
|
||||
"defaultMessage": "일정 일시중지 해제"
|
||||
},
|
||||
|
||||
@@ -3296,6 +3296,9 @@
|
||||
"scheduleDetailView.cannotModifyRunning": {
|
||||
"defaultMessage": "Нельзя запускать или изменять расписание, пока оно выполняется."
|
||||
},
|
||||
"scheduleDetailView.completedSession": {
|
||||
"defaultMessage": "Сеанс: {sessionId}"
|
||||
},
|
||||
"scheduleDetailView.created": {
|
||||
"defaultMessage": "Создано: {date}"
|
||||
},
|
||||
@@ -3317,9 +3320,6 @@
|
||||
"scheduleDetailView.errorPrefix": {
|
||||
"defaultMessage": "Ошибка: {error}"
|
||||
},
|
||||
"scheduleDetailView.failedToLoadSession": {
|
||||
"defaultMessage": "Не удалось загрузить сеанс"
|
||||
},
|
||||
"scheduleDetailView.idLabel": {
|
||||
"defaultMessage": "ID:"
|
||||
},
|
||||
@@ -3362,9 +3362,6 @@
|
||||
"scheduleDetailView.messages": {
|
||||
"defaultMessage": "Сообщений: {count}"
|
||||
},
|
||||
"scheduleDetailView.newSession": {
|
||||
"defaultMessage": "Новый сеанс: {sessionId}"
|
||||
},
|
||||
"scheduleDetailView.noScheduleId": {
|
||||
"defaultMessage": "ID расписания не указан. Вернитесь к списку расписаний."
|
||||
},
|
||||
@@ -3401,6 +3398,9 @@
|
||||
"scheduleDetailView.runScheduleNow": {
|
||||
"defaultMessage": "Запустить расписание сейчас"
|
||||
},
|
||||
"scheduleDetailView.scheduleCompleted": {
|
||||
"defaultMessage": "Запуск завершен"
|
||||
},
|
||||
"scheduleDetailView.scheduleDetails": {
|
||||
"defaultMessage": "Детали расписания"
|
||||
},
|
||||
@@ -3419,9 +3419,6 @@
|
||||
"scheduleDetailView.schedulePaused": {
|
||||
"defaultMessage": "Расписание приостановлено"
|
||||
},
|
||||
"scheduleDetailView.scheduleTriggered": {
|
||||
"defaultMessage": "Расписание запущено"
|
||||
},
|
||||
"scheduleDetailView.scheduleUnpaused": {
|
||||
"defaultMessage": "Расписание возобновлено"
|
||||
},
|
||||
@@ -3431,9 +3428,6 @@
|
||||
"scheduleDetailView.sessionId": {
|
||||
"defaultMessage": "ID сеанса: {id}"
|
||||
},
|
||||
"scheduleDetailView.tokens": {
|
||||
"defaultMessage": "Токенов: {count}"
|
||||
},
|
||||
"scheduleDetailView.unpauseSchedule": {
|
||||
"defaultMessage": "Возобновить расписание"
|
||||
},
|
||||
|
||||
@@ -3296,6 +3296,9 @@
|
||||
"scheduleDetailView.cannotModifyRunning": {
|
||||
"defaultMessage": "Zaten çalışmakta olan bir program tetiklenemez veya değiştirilemez."
|
||||
},
|
||||
"scheduleDetailView.completedSession": {
|
||||
"defaultMessage": "Oturum: {sessionId}"
|
||||
},
|
||||
"scheduleDetailView.created": {
|
||||
"defaultMessage": "Oluşturuldu: {date}"
|
||||
},
|
||||
@@ -3317,9 +3320,6 @@
|
||||
"scheduleDetailView.errorPrefix": {
|
||||
"defaultMessage": "Hata: {error}"
|
||||
},
|
||||
"scheduleDetailView.failedToLoadSession": {
|
||||
"defaultMessage": "Oturum yüklenemedi"
|
||||
},
|
||||
"scheduleDetailView.idLabel": {
|
||||
"defaultMessage": "Kimlik:"
|
||||
},
|
||||
@@ -3362,9 +3362,6 @@
|
||||
"scheduleDetailView.messages": {
|
||||
"defaultMessage": "Mesajlar: {count}"
|
||||
},
|
||||
"scheduleDetailView.newSession": {
|
||||
"defaultMessage": "Yeni oturum: {sessionId}"
|
||||
},
|
||||
"scheduleDetailView.noScheduleId": {
|
||||
"defaultMessage": "Program kimliği sağlanmadı. Program listesine geri dönün."
|
||||
},
|
||||
@@ -3401,6 +3398,9 @@
|
||||
"scheduleDetailView.runScheduleNow": {
|
||||
"defaultMessage": "Şimdi Planla'yı Çalıştır"
|
||||
},
|
||||
"scheduleDetailView.scheduleCompleted": {
|
||||
"defaultMessage": "Çalıştırma tamamlandı"
|
||||
},
|
||||
"scheduleDetailView.scheduleDetails": {
|
||||
"defaultMessage": "Program Ayrıntıları"
|
||||
},
|
||||
@@ -3419,9 +3419,6 @@
|
||||
"scheduleDetailView.schedulePaused": {
|
||||
"defaultMessage": "Plan Duraklatıldı"
|
||||
},
|
||||
"scheduleDetailView.scheduleTriggered": {
|
||||
"defaultMessage": "Zamanlama Tetiklendi"
|
||||
},
|
||||
"scheduleDetailView.scheduleUnpaused": {
|
||||
"defaultMessage": "Duraklatmayı Kaldırmayı Planla"
|
||||
},
|
||||
@@ -3431,9 +3428,6 @@
|
||||
"scheduleDetailView.sessionId": {
|
||||
"defaultMessage": "Oturum Kimliği: {id}"
|
||||
},
|
||||
"scheduleDetailView.tokens": {
|
||||
"defaultMessage": "Jetonlar: {count}"
|
||||
},
|
||||
"scheduleDetailView.unpauseSchedule": {
|
||||
"defaultMessage": "Programı Duraklatmayı Kaldır"
|
||||
},
|
||||
|
||||
@@ -3296,6 +3296,9 @@
|
||||
"scheduleDetailView.cannotModifyRunning": {
|
||||
"defaultMessage": "计划正在运行中,无法触发或修改。"
|
||||
},
|
||||
"scheduleDetailView.completedSession": {
|
||||
"defaultMessage": "会话:{sessionId}"
|
||||
},
|
||||
"scheduleDetailView.created": {
|
||||
"defaultMessage": "创建于:{date}"
|
||||
},
|
||||
@@ -3317,9 +3320,6 @@
|
||||
"scheduleDetailView.errorPrefix": {
|
||||
"defaultMessage": "错误:{error}"
|
||||
},
|
||||
"scheduleDetailView.failedToLoadSession": {
|
||||
"defaultMessage": "加载会话失败"
|
||||
},
|
||||
"scheduleDetailView.idLabel": {
|
||||
"defaultMessage": "ID:"
|
||||
},
|
||||
@@ -3362,9 +3362,6 @@
|
||||
"scheduleDetailView.messages": {
|
||||
"defaultMessage": "消息:{count}"
|
||||
},
|
||||
"scheduleDetailView.newSession": {
|
||||
"defaultMessage": "新会话:{sessionId}"
|
||||
},
|
||||
"scheduleDetailView.noScheduleId": {
|
||||
"defaultMessage": "未提供计划 ID。返回计划列表。"
|
||||
},
|
||||
@@ -3401,6 +3398,9 @@
|
||||
"scheduleDetailView.runScheduleNow": {
|
||||
"defaultMessage": "立即运行计划"
|
||||
},
|
||||
"scheduleDetailView.scheduleCompleted": {
|
||||
"defaultMessage": "运行已完成"
|
||||
},
|
||||
"scheduleDetailView.scheduleDetails": {
|
||||
"defaultMessage": "计划详情"
|
||||
},
|
||||
@@ -3419,9 +3419,6 @@
|
||||
"scheduleDetailView.schedulePaused": {
|
||||
"defaultMessage": "计划已暂停"
|
||||
},
|
||||
"scheduleDetailView.scheduleTriggered": {
|
||||
"defaultMessage": "计划已触发"
|
||||
},
|
||||
"scheduleDetailView.scheduleUnpaused": {
|
||||
"defaultMessage": "计划已恢复"
|
||||
},
|
||||
@@ -3431,9 +3428,6 @@
|
||||
"scheduleDetailView.sessionId": {
|
||||
"defaultMessage": "会话 ID:{id}"
|
||||
},
|
||||
"scheduleDetailView.tokens": {
|
||||
"defaultMessage": "令牌:{count}"
|
||||
},
|
||||
"scheduleDetailView.unpauseSchedule": {
|
||||
"defaultMessage": "恢复计划"
|
||||
},
|
||||
|
||||
@@ -1,191 +0,0 @@
|
||||
import {
|
||||
listSchedules as apiListSchedules,
|
||||
createSchedule as apiCreateSchedule,
|
||||
deleteSchedule as apiDeleteSchedule,
|
||||
pauseSchedule as apiPauseSchedule,
|
||||
unpauseSchedule as apiUnpauseSchedule,
|
||||
updateSchedule as apiUpdateSchedule,
|
||||
sessionsHandler as apiGetScheduleSessions,
|
||||
runNowHandler as apiRunScheduleNow,
|
||||
killRunningJob as apiKillRunningJob,
|
||||
inspectRunningJob as apiInspectRunningJob,
|
||||
SessionDisplayInfo,
|
||||
} from './api';
|
||||
import type { Recipe } from './recipe';
|
||||
|
||||
export interface ScheduledJob {
|
||||
id: string;
|
||||
source: string;
|
||||
cron: string;
|
||||
last_run?: string | null;
|
||||
currently_running?: boolean;
|
||||
paused?: boolean;
|
||||
current_session_id?: string | null;
|
||||
process_start_time?: string | null;
|
||||
}
|
||||
|
||||
export interface ScheduleSession {
|
||||
id: string;
|
||||
name: string;
|
||||
createdAt: string; // ISO 8601 date string
|
||||
workingDir: string;
|
||||
scheduleId: string;
|
||||
messageCount: number;
|
||||
totalTokens: number;
|
||||
inputTokens: number;
|
||||
outputTokens: number;
|
||||
accumulatedTotalTokens: number;
|
||||
accumulatedInputTokens: number;
|
||||
accumulatedOutputTokens: number;
|
||||
}
|
||||
|
||||
export async function listSchedules(): Promise<ScheduledJob[]> {
|
||||
try {
|
||||
const response = await apiListSchedules<true>();
|
||||
if (response && response.data && Array.isArray(response.data.jobs)) {
|
||||
return response.data.jobs as ScheduledJob[];
|
||||
}
|
||||
console.error('Unexpected response format from apiListSchedules', response);
|
||||
throw new Error('Failed to list schedules: Unexpected response format');
|
||||
} catch (error) {
|
||||
console.error('Error listing schedules:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function createSchedule(request: {
|
||||
id: string;
|
||||
recipe: Recipe;
|
||||
cron: string;
|
||||
}): Promise<ScheduledJob> {
|
||||
type ApiCreateScheduleBody = Parameters<typeof apiCreateSchedule>[0]['body'];
|
||||
const response = await apiCreateSchedule({ body: request as unknown as ApiCreateScheduleBody });
|
||||
if (response.data) {
|
||||
return response.data as ScheduledJob;
|
||||
}
|
||||
const err = response.error as { message?: string } | undefined;
|
||||
throw new Error(err?.message || 'Failed to create schedule');
|
||||
}
|
||||
|
||||
export async function deleteSchedule(id: string): Promise<void> {
|
||||
try {
|
||||
await apiDeleteSchedule<true>({ path: { id } });
|
||||
} catch (error) {
|
||||
console.error(`Error deleting schedule ${id}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getScheduleSessions(
|
||||
scheduleId: string,
|
||||
limit: number
|
||||
): Promise<Array<SessionDisplayInfo>> {
|
||||
const response = await apiGetScheduleSessions<true>({
|
||||
path: { id: scheduleId },
|
||||
query: { limit },
|
||||
throwOnError: true,
|
||||
});
|
||||
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function runScheduleNow(scheduleId: string): Promise<string> {
|
||||
try {
|
||||
const response = await apiRunScheduleNow<true>({
|
||||
path: { id: scheduleId },
|
||||
});
|
||||
|
||||
if (response && response.data && response.data.session_id) {
|
||||
return response.data.session_id;
|
||||
}
|
||||
console.error('Unexpected response format from apiRunScheduleNow', response);
|
||||
throw new Error('Failed to run schedule now: Unexpected response format');
|
||||
} catch (error) {
|
||||
console.error(`Error running schedule ${scheduleId} now:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function pauseSchedule(scheduleId: string): Promise<void> {
|
||||
try {
|
||||
await apiPauseSchedule<true>({
|
||||
path: { id: scheduleId },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Error pausing schedule ${scheduleId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function unpauseSchedule(scheduleId: string): Promise<void> {
|
||||
try {
|
||||
await apiUnpauseSchedule<true>({
|
||||
path: { id: scheduleId },
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Error unpausing schedule ${scheduleId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function updateSchedule(scheduleId: string, cron: string): Promise<ScheduledJob> {
|
||||
try {
|
||||
const response = await apiUpdateSchedule<true>({
|
||||
path: { id: scheduleId },
|
||||
body: { cron },
|
||||
});
|
||||
|
||||
if (response && response.data) {
|
||||
return response.data as ScheduledJob;
|
||||
}
|
||||
console.error('Unexpected response format from apiUpdateSchedule', response);
|
||||
throw new Error('Failed to update schedule: Unexpected response format');
|
||||
} catch (error) {
|
||||
console.error(`Error updating schedule ${scheduleId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export interface KillJobResponse {
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface InspectJobResponse {
|
||||
sessionId?: string | null;
|
||||
processStartTime?: string | null;
|
||||
runningDurationSeconds?: number | null;
|
||||
}
|
||||
|
||||
export async function killRunningJob(scheduleId: string): Promise<KillJobResponse> {
|
||||
try {
|
||||
const response = await apiKillRunningJob<true>({
|
||||
path: { id: scheduleId },
|
||||
});
|
||||
|
||||
if (response && response.data) {
|
||||
return response.data as KillJobResponse;
|
||||
}
|
||||
console.error('Unexpected response format from apiKillRunningJob', response);
|
||||
throw new Error('Failed to kill running job: Unexpected response format');
|
||||
} catch (error) {
|
||||
console.error(`Error killing running job ${scheduleId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function inspectRunningJob(scheduleId: string): Promise<InspectJobResponse> {
|
||||
try {
|
||||
const response = await apiInspectRunningJob<true>({
|
||||
path: { id: scheduleId },
|
||||
});
|
||||
|
||||
if (response && response.data) {
|
||||
return response.data as InspectJobResponse;
|
||||
}
|
||||
console.error('Unexpected response format from apiInspectRunningJob', response);
|
||||
throw new Error('Failed to inspect running job: Unexpected response format');
|
||||
} catch (error) {
|
||||
console.error(`Error inspecting running job ${scheduleId}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user