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:
Richard ZinkandClaude Opus 5.5 committed 2026-11-10 10:05:15 +01:00
1 parent 872ebb94dd
commit 05c215f685
19 files changed
+1661 -356

No files matched your search

+128
View File
@@ -0,0 +1,128 @@
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>
);
}