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>
129 lines
4.2 KiB
TypeScript
129 lines
4.2 KiB
TypeScript
import { useState } from "react";
|
||
import { post } from "../api";
|
||
import { getLang, useT } from "../i18n";
|
||
import { JOB_TEMPLATES, type JobTemplate } from "../jobTemplates";
|
||
import { navigate } from "../router";
|
||
import { Icon } from "./Icon";
|
||
import { FolderPicker } from "./NewSession";
|
||
|
||
/** Starting point of a new instructions file: headings to fill in (with the assistant). */
|
||
function example(name: string, de: boolean) {
|
||
return de
|
||
? `# ${name}
|
||
|
||
## Ziel
|
||
Was soll diese Automatisierung erreichen? (ein, zwei Sätze)
|
||
|
||
## Vorgehen
|
||
1. …
|
||
2. …
|
||
|
||
## Ergebnis
|
||
Was am Ende vorliegen soll – z. B. ein kurzer Bericht, geänderte Dateien, ein Commit.
|
||
|
||
## Grenzen
|
||
- Was der Agent nicht tun darf (z. B. nichts pushen, nichts löschen).
|
||
`
|
||
: `# ${name}
|
||
|
||
## Goal
|
||
What should this automation achieve? (one or two sentences)
|
||
|
||
## Steps
|
||
1. …
|
||
2. …
|
||
|
||
## Result
|
||
What should exist at the end – e.g. a short report, changed files, a commit.
|
||
|
||
## Limits
|
||
- What the agent must not do (e.g. no pushing, no deleting).
|
||
`;
|
||
}
|
||
|
||
/**
|
||
* New automation: name, project and a starting point (example or template). Creates the
|
||
* instructions file (<project>/.codecraft/jobs/<name>.md, switched off) and opens the
|
||
* editor, where the schedule is set and the instructions are worked out.
|
||
*/
|
||
export function NewJobDialog({ onClose }: { onClose: () => void }) {
|
||
const t = useT();
|
||
const de = getLang() === "de";
|
||
const [tpl, setTpl] = useState<JobTemplate>();
|
||
const [name, setName] = useState("");
|
||
const [cwd, setCwd] = useState(localStorage.getItem("codecraft.lastDir") ?? "");
|
||
const [error, setError] = useState<string>();
|
||
const [busy, setBusy] = useState(false);
|
||
|
||
const choose = (x?: JobTemplate) => {
|
||
setTpl(x);
|
||
if (x && (!name.trim() || JOB_TEMPLATES.some((y) => name === y.name.de || name === y.name.en))) setName(de ? x.name.de : x.name.en);
|
||
};
|
||
|
||
const create = async () => {
|
||
setBusy(true);
|
||
setError(undefined);
|
||
try {
|
||
const n = name.trim();
|
||
const job = await post<{ id: string }>("/jobs/create", {
|
||
cwd,
|
||
name: n,
|
||
prompt: tpl ? `# ${n}\n\n${tpl.prompt}\n` : example(n, de),
|
||
schedule: tpl?.schedule,
|
||
agent: tpl?.agent,
|
||
});
|
||
onClose();
|
||
navigate(`/jobs/${job.id}`);
|
||
} catch (e: any) {
|
||
setError(e.message);
|
||
} finally {
|
||
setBusy(false);
|
||
}
|
||
};
|
||
|
||
return (
|
||
<div className="modal-backdrop" onClick={onClose}>
|
||
<div className="modal job-tpl" role="dialog" aria-label={t("jobs.new")} onClick={(e) => e.stopPropagation()}>
|
||
<h3>{t("jobs.new")}</h3>
|
||
<label className="field">
|
||
<span>{t("jobs.tplName")}</span>
|
||
<input autoFocus value={name} placeholder={t("jobs.namePlaceholder")} onChange={(e) => setName(e.target.value)} />
|
||
</label>
|
||
<div className="field">
|
||
<span>{t("jobs.tplProject")}</span>
|
||
<FolderPicker path={cwd} onPick={setCwd} />
|
||
<small className="hint">{t("jobs.fileHint")}</small>
|
||
</div>
|
||
<div className="field">
|
||
<span>{t("jobs.startWith")}</span>
|
||
<div className="job-tpl-grid">
|
||
<button className={`job-tpl-card ${!tpl ? "active" : ""}`} onClick={() => choose(undefined)}>
|
||
<Icon name="file-text" />
|
||
<span>
|
||
<b>{t("jobs.blank")}</b>
|
||
<small>{t("jobs.blankDesc")}</small>
|
||
</span>
|
||
</button>
|
||
{JOB_TEMPLATES.map((x) => (
|
||
<button key={x.id} className={`job-tpl-card ${tpl?.id === x.id ? "active" : ""}`} onClick={() => choose(x)}>
|
||
<Icon name={x.icon} />
|
||
<span>
|
||
<b>{de ? x.name.de : x.name.en}</b>
|
||
<small>{de ? x.desc.de : x.desc.en}</small>
|
||
</span>
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
{error && <div className="notice err">{error}</div>}
|
||
<div className="row" style={{ marginTop: 12, justifyContent: "flex-end" }}>
|
||
<button onClick={onClose}>{t("common.cancel")}</button>
|
||
<button className="primary" disabled={busy || !cwd || !name.trim()} onClick={create}>
|
||
{t("jobs.createAndEdit")}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|