Automation editor with graphical schedule and assistant; movable chat list groups
Automations
- Job files hold only the instructions; name, schedule, mode, model, approval
mode and time limit are settings in the database (jobs.settings). Existing
files are adopted on start-up (frontmatter into the database and removed from
the file); plain Markdown files registered later get default settings and stay
switched off until scheduled.
- New automation: name, project and a starting point (blank example structure or
a template) create .codecraft/jobs/<name>.md and open the editor: settings with
a graphical cron builder (next runs in server time via /jobs/schedule), the
instructions in the Markdown editor and an assistant chat
(POST /jobs/{id}/assistant) whose model edits the file; the editor reloads it
after each turn and warns about conflicts with unsaved edits.
- The schedule tool writes prompt-only files with settings.
Chat list
- Folders and projects keep a user-defined order and can be pinned (drag and
drop, or the group's menu), stored per user via /account/sidebar. A new
message moves the chat to the top of its group, no longer the project itself.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
872ebb94dd
commit
05c215f685
19 files changed
+1661
-356
No files matched your search
+16
-40
@@ -5,9 +5,9 @@ import { navigate } from "../router";
|
||||
import { confirmDialog, toast } from "../dialogs";
|
||||
import { Icon } from "./Icon";
|
||||
import { Spinner } from "./Activity";
|
||||
import { JobTemplateDialog } from "./JobTemplateDialog";
|
||||
import { NewJobDialog } from "./NewJobDialog";
|
||||
|
||||
interface JobInfo {
|
||||
export interface JobInfo {
|
||||
id: string;
|
||||
path: string;
|
||||
enabled: boolean;
|
||||
@@ -17,12 +17,12 @@ interface JobInfo {
|
||||
next_run?: number | null;
|
||||
error?: string;
|
||||
webhook?: boolean;
|
||||
spec?: { name: string; schedule: string; agent: string; model?: string | null; permission_mode: string; timeout_minutes: number; prompt: string };
|
||||
spec?: { name: string; schedule: string; agent: string; model?: string | null; permission_mode: string; timeout_minutes: number; prompt: string; assistant_session?: string | null };
|
||||
}
|
||||
|
||||
const fmtDate = (ms?: number | null) => (ms ? new Date(ms).toLocaleString(undefined, { dateStyle: "medium", timeStyle: "short" }) : "–");
|
||||
|
||||
/** Automations: agents started on a schedule from a Markdown job file. */
|
||||
/** Automations: agents started on a schedule with the instructions from a Markdown file. */
|
||||
export function Jobs({ onMenu }: { onMenu: () => void }) {
|
||||
const t = useT();
|
||||
const [jobs, setJobs] = useState<JobInfo[]>();
|
||||
@@ -68,10 +68,10 @@ export function Jobs({ onMenu }: { onMenu: () => void }) {
|
||||
<div className="notice">{t("jobs.intro")}</div>
|
||||
<div className="row" style={{ margin: "12px 0" }}>
|
||||
<button className="primary" onClick={() => setTemplates(true)}>
|
||||
<Icon name="plus" /> {t("jobs.fromTemplate")}
|
||||
<Icon name="plus" /> {t("jobs.new")}
|
||||
</button>
|
||||
</div>
|
||||
{templates && <JobTemplateDialog onClose={() => setTemplates(false)} onCreated={() => void reload()} />}
|
||||
{templates && <NewJobDialog onClose={() => setTemplates(false)} />}
|
||||
{!jobs && <div className="empty"><Spinner /></div>}
|
||||
{jobs?.length === 0 && <div className="empty">{t("jobs.none")}</div>}
|
||||
{jobs?.map((j) => (
|
||||
@@ -79,7 +79,11 @@ export function Jobs({ onMenu }: { onMenu: () => void }) {
|
||||
<div className="job-head">
|
||||
<Icon name="clock" className="job-icon" />
|
||||
<div style={{ flex: 1, minWidth: 0 }}>
|
||||
<h3>{j.spec?.name ?? j.path.split("/").pop()}</h3>
|
||||
<h3>
|
||||
<button className="link-btn job-title" onClick={() => navigate(`/jobs/${j.id}`)}>
|
||||
{j.spec?.name ?? j.path.split("/").pop()}
|
||||
</button>
|
||||
</h3>
|
||||
<div className="hint mono">{j.path}</div>
|
||||
</div>
|
||||
<label className="switch" title={t(j.enabled ? "jobs.disable" : "jobs.enable")}>
|
||||
@@ -107,22 +111,7 @@ export function Jobs({ onMenu }: { onMenu: () => void }) {
|
||||
</span>
|
||||
<span>{t("jobs.mode")}</span>
|
||||
<span>
|
||||
{j.spec?.agent} · {j.spec?.model || t("agents.sessionModel")} · max {j.spec?.timeout_minutes} min
|
||||
</span>
|
||||
<span>{t("jobs.permissionMode")}</span>
|
||||
<span>
|
||||
<select
|
||||
value={j.spec?.permission_mode ?? "auto"}
|
||||
disabled={busy === j.id}
|
||||
title={t("jobs.permissionModeHint")}
|
||||
aria-label={t("jobs.permissionMode")}
|
||||
onChange={(e) => act(j.id, () => patch(`/jobs/${j.id}`, { permission_mode: e.target.value }))}
|
||||
>
|
||||
<option value="default">{t("permissionMode.default")}</option>
|
||||
<option value="auto">{t("permissionMode.auto")}</option>
|
||||
<option value="accept_edits">{t("permissionMode.accept_edits")}</option>
|
||||
<option value="bypass">{t("permissionMode.bypass")}</option>
|
||||
</select>
|
||||
{j.spec?.agent} · {j.spec?.model || t("agents.sessionModel")} · {t(`permissionMode.${j.spec?.permission_mode ?? "auto"}` as any)} · max {j.spec?.timeout_minutes} min
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -178,14 +167,8 @@ export function Jobs({ onMenu }: { onMenu: () => void }) {
|
||||
})}>
|
||||
{busy === j.id ? <Spinner small /> : <Icon name="play" />} {t("jobs.runNow")}
|
||||
</button>
|
||||
<button
|
||||
className="small ghost"
|
||||
onClick={() => {
|
||||
sessionStorage.setItem("codecraft.openFile", j.path);
|
||||
navigate("/files");
|
||||
}}
|
||||
>
|
||||
<Icon name="edit" /> {t("jobs.editFile")}
|
||||
<button className="small" onClick={() => navigate(`/jobs/${j.id}`)}>
|
||||
<Icon name="edit" /> {t("jobs.edit")}
|
||||
</button>
|
||||
<span className="grow" />
|
||||
<button
|
||||
@@ -205,20 +188,13 @@ export function Jobs({ onMenu }: { onMenu: () => void }) {
|
||||
<div className="row" style={{ gap: 8 }}>
|
||||
<input className="mono" style={{ flex: 1, minWidth: 0 }} value={path} placeholder="/workspace/project/.codecraft/jobs/nightly.md" onChange={(e) => setPath(e.target.value)} />
|
||||
<button disabled={!path.trim()} onClick={() => act("new", async () => {
|
||||
await post("/jobs", { path: path.trim() });
|
||||
const j = await post<JobInfo>("/jobs", { path: path.trim() });
|
||||
setPath("");
|
||||
navigate(`/jobs/${j.id}`);
|
||||
})}>
|
||||
{t("jobs.registerBtn")}
|
||||
</button>
|
||||
</div>
|
||||
<pre className="job-example">{`---
|
||||
name: "Nightly dependency check"
|
||||
schedule: "0 3 * * *"
|
||||
agent: build
|
||||
permission_mode: auto
|
||||
timeout_minutes: 60
|
||||
---
|
||||
Check the project for outdated dependencies …`}</pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user