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:
Naiyuan Qing 2026-02-09 13:44:08 +08:00
commit 23905daaa1
85 changed files with 7368 additions and 470 deletions

View 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

View 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,
};
}

View file

@ -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',
}