Merge remote-tracking branch 'origin/main' into feat/telegram-channel
# Conflicts: # apps/desktop/electron/electron-env.d.ts # apps/desktop/electron/ipc/index.ts # apps/desktop/electron/preload.ts # apps/desktop/src/App.tsx # apps/desktop/src/pages/layout.tsx # src/agent/async-agent.ts # src/agent/runner.ts # src/hub/hub.ts
This commit is contained in:
commit
23905daaa1
85 changed files with 7368 additions and 470 deletions
107
apps/desktop/src/hooks/use-cron-jobs.ts
Normal file
107
apps/desktop/src/hooks/use-cron-jobs.ts
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
import { useState, useEffect, useCallback } from 'react'
|
||||
|
||||
export interface CronJobInfo {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
enabled: boolean
|
||||
schedule: string
|
||||
sessionTarget: string
|
||||
nextRunAt: string | null
|
||||
lastStatus: 'ok' | 'error' | 'skipped' | null
|
||||
lastRunAt: string | null
|
||||
lastDurationMs: number | null
|
||||
lastError: string | null
|
||||
}
|
||||
|
||||
export interface UseCronJobsReturn {
|
||||
jobs: CronJobInfo[]
|
||||
loading: boolean
|
||||
error: string | null
|
||||
toggleJob: (jobId: string) => Promise<void>
|
||||
removeJob: (jobId: string) => Promise<void>
|
||||
refresh: () => Promise<void>
|
||||
}
|
||||
|
||||
export function useCronJobs(): UseCronJobsReturn {
|
||||
const [jobs, setJobs] = useState<CronJobInfo[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const fetchJobs = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
|
||||
const result = window.electronAPI
|
||||
? await window.electronAPI.cron.list()
|
||||
: await window.ipcRenderer.invoke('cron:list')
|
||||
|
||||
if (Array.isArray(result)) {
|
||||
setJobs(result)
|
||||
} else {
|
||||
setError('Invalid response from cron:list')
|
||||
}
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to fetch cron jobs')
|
||||
setJobs([])
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
fetchJobs()
|
||||
}, [fetchJobs])
|
||||
|
||||
const toggleJob = useCallback(async (jobId: string) => {
|
||||
try {
|
||||
const result = window.electronAPI
|
||||
? await window.electronAPI.cron.toggle(jobId)
|
||||
: await window.ipcRenderer.invoke('cron:toggle', jobId)
|
||||
|
||||
const typed = result as { error?: string; id?: string; enabled?: boolean }
|
||||
if (typed.error) {
|
||||
setError(typed.error)
|
||||
return
|
||||
}
|
||||
|
||||
setJobs((prev) =>
|
||||
prev.map((job) =>
|
||||
job.id === jobId ? { ...job, enabled: typed.enabled ?? !job.enabled } : job
|
||||
)
|
||||
)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to toggle job')
|
||||
}
|
||||
}, [])
|
||||
|
||||
const removeJob = useCallback(async (jobId: string) => {
|
||||
try {
|
||||
const result = window.electronAPI
|
||||
? await window.electronAPI.cron.remove(jobId)
|
||||
: await window.ipcRenderer.invoke('cron:remove', jobId)
|
||||
|
||||
const typed = result as { error?: string; ok?: boolean }
|
||||
if (typed.error) {
|
||||
setError(typed.error)
|
||||
return
|
||||
}
|
||||
|
||||
setJobs((prev) => prev.filter((job) => job.id !== jobId))
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Failed to remove job')
|
||||
}
|
||||
}, [])
|
||||
|
||||
return {
|
||||
jobs,
|
||||
loading,
|
||||
error,
|
||||
toggleJob,
|
||||
removeJob,
|
||||
refresh: fetchJobs,
|
||||
}
|
||||
}
|
||||
|
||||
export default useCronJobs
|
||||
70
apps/desktop/src/hooks/use-heartbeat.ts
Normal file
70
apps/desktop/src/hooks/use-heartbeat.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
export type HeartbeatEvent = {
|
||||
ts: number;
|
||||
status: "sent" | "ok-empty" | "ok-token" | "skipped" | "failed";
|
||||
preview?: string;
|
||||
durationMs?: number;
|
||||
reason?: string;
|
||||
};
|
||||
|
||||
export function useHeartbeat() {
|
||||
const [enabled, setEnabled] = useState(true);
|
||||
const [lastEvent, setLastEvent] = useState<HeartbeatEvent | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const refresh = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const event = (await window.electronAPI.heartbeat.last()) as HeartbeatEvent | null;
|
||||
setLastEvent(event);
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
void refresh();
|
||||
const timer = setInterval(() => {
|
||||
void refresh();
|
||||
}, 15000);
|
||||
return () => clearInterval(timer);
|
||||
}, [refresh]);
|
||||
|
||||
const toggleEnabled = useCallback(async () => {
|
||||
const next = !enabled;
|
||||
const result = await window.electronAPI.heartbeat.setEnabled(next);
|
||||
if (result.ok) {
|
||||
setEnabled(next);
|
||||
} else {
|
||||
setError(result.error ?? "Failed to update heartbeat setting");
|
||||
}
|
||||
}, [enabled]);
|
||||
|
||||
const wakeNow = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const result = await window.electronAPI.heartbeat.wake("manual");
|
||||
if (!result.ok) {
|
||||
setError(result.error ?? "Failed to run heartbeat");
|
||||
}
|
||||
await refresh();
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [refresh]);
|
||||
|
||||
return {
|
||||
enabled,
|
||||
lastEvent,
|
||||
loading,
|
||||
error,
|
||||
refresh,
|
||||
toggleEnabled,
|
||||
wakeNow,
|
||||
};
|
||||
}
|
||||
|
|
@ -31,6 +31,8 @@ const TOOL_DESCRIPTIONS: Record<string, string> = {
|
|||
memory_set: 'Store a memory value',
|
||||
memory_delete: 'Delete a memory value',
|
||||
memory_list: 'List all memory keys',
|
||||
memory_search: 'Search memory files for keywords',
|
||||
cron: 'Create and manage scheduled tasks',
|
||||
}
|
||||
|
||||
// Group display names
|
||||
|
|
@ -39,6 +41,8 @@ const GROUP_NAMES: Record<string, string> = {
|
|||
runtime: 'Runtime',
|
||||
web: 'Web',
|
||||
memory: 'Memory',
|
||||
subagent: 'Subagent',
|
||||
cron: 'Cron',
|
||||
other: 'Other',
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue