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

+16 -40
View File
@@ -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>