Skip to content
Open
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
013edda
feat(frontend): replace default workspace look with classic UI
LittleChenLiya Aug 18, 2026
8d3b1b2
feat(frontend): add observatory workspace skin
LittleChenLiya Aug 19, 2026
16422d1
fix(frontend): remove observatory sky-toggle button from workspace he…
LittleChenLiya Aug 20, 2026
6426419
chore: checkpoint current frontend UI rework (WIP checkpoint)
LittleChenLiya Aug 20, 2026
0f2de46
feat(frontend): classic skin + UI rework (observatory stripped)
LittleChenLiya Aug 20, 2026
72b9561
fix(frontend): constrain classic skin card width in appearance settings
LittleChenLiya Aug 20, 2026
cf3c545
style(frontend): prettier format for CI
LittleChenLiya Aug 20, 2026
85b2ba0
chore(frontend): remove stray scratch files from checkpoint
LittleChenLiya Aug 20, 2026
30e8435
refactor(frontend): remove dead observatory i18n keys from PR1
LittleChenLiya Aug 20, 2026
e863f95
refactor(frontend): scheduled tasks page to card grid + detail dialog
LittleChenLiya Aug 20, 2026
441c807
fix(frontend): address review findings - destructive contrast, chats …
LittleChenLiya Aug 20, 2026
d26a7fd
feat(frontend): show icon header on create scheduled task page
LittleChenLiya Aug 20, 2026
1a5f32e
fix(frontend): restore destructive contrast in dark theme
LittleChenLiya Aug 20, 2026
3b030e3
refactor(frontend): remove dead css vars, redundant header wrapper, d…
LittleChenLiya Aug 21, 2026
d44673a
refactor(frontend): move skin scaffolding out of PR1 (SkinProvider/Pa…
LittleChenLiya Aug 21, 2026
a4e03d2
refactor(frontend): split out non-skin extras (chats settings page, z…
LittleChenLiya Aug 21, 2026
9cb7f4e
style(frontend): vertically center new scheduled task form
LittleChenLiya Aug 21, 2026
158be8c
fix(frontend): address review feedback on scheduled-tasks redesign an…
LittleChenLiya Aug 21, 2026
4f14ad9
refactor(frontend): remove orphaned memory/workspace i18n keys and re…
LittleChenLiya Aug 21, 2026
e433ad0
feat(frontend): timezone dropdown with common+all zones, and e2e fixes
LittleChenLiya Aug 21, 2026
c46d4a8
fix(frontend): forward thread_id to create page, extract run-at valid…
LittleChenLiya Aug 22, 2026
6fae72c
fix(frontend): guard edit submit against empty schedule spec, announc…
LittleChenLiya Aug 22, 2026
acca5a2
fix(frontend): gate empty state on query pending, drop dead form-erro…
LittleChenLiya Aug 22, 2026
112be92
fix(frontend): use nullish coalescing for last task error fallback
LittleChenLiya Aug 22, 2026
49b5ad7
fix(frontend): drop orphan fillRequired key, mirror invalidOnce hint …
LittleChenLiya Aug 22, 2026
4c22a5e
refactor(frontend): drop orphan lastRunId i18n key
LittleChenLiya Aug 22, 2026
78f3d19
fix(frontend): restore reuse-thread context notice after rebase onto …
LittleChenLiya Aug 24, 2026
91a0872
test(frontend): close detail dialog before navigating to create page …
LittleChenLiya Aug 24, 2026
d8da426
fix(frontend): preserve thread_id on create exits, label back button,…
LittleChenLiya Aug 24, 2026
bd73174
fix(frontend): return created scheduled tasks to a list that can cont…
LittleChenLiya Aug 24, 2026
bbd2aae
merge origin/main into feat/frontend-ui-rework
LittleChenLiya Aug 25, 2026
eb7ee01
merge origin/main and port scheduled-task duplication
LittleChenLiya Aug 28, 2026
b5f19c0
Merge remote-tracking branch 'origin/main' into feat/frontend-ui-rework
LittleChenLiya Aug 30, 2026
bbf1096
fix(frontend): keep scheduled-task duplicate drafts out of URLs
LittleChenLiya Aug 30, 2026
e11d5fa
Merge remote-tracking branch 'origin/main' into feat/frontend-ui-rework
LittleChenLiya Aug 30, 2026
05a1fe0
Merge remote-tracking branch 'origin/main' into feat/frontend-ui-rework
LittleChenLiya Aug 30, 2026
78dd7a5
fix(frontend): restore describeSchedule coverage dropped in rebase fa…
LittleChenLiya Aug 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
182 changes: 182 additions & 0 deletions frontend/src/app/workspace/scheduled-tasks/new/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
"use client";

import { ArrowLeftIcon, CalendarClock, TriangleAlertIcon } from "lucide-react";
import { useRouter, useSearchParams } from "next/navigation";
import { useState } from "react";

import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import {
ScheduledTaskScheduleInput,
type ScheduleValue,
} from "@/components/workspace/scheduled-task-schedule-input";
import { useI18n } from "@/core/i18n/hooks";
import { useCreateScheduledTask } from "@/core/scheduled-tasks/hooks";

function ReuseThreadNotice({
title,
description,
}: {
title: string;
description: string;
}) {
return (
<Alert className="border-amber-500/50 bg-amber-500/10">
<TriangleAlertIcon className="text-amber-600 dark:text-amber-400" />
<AlertTitle>{title}</AlertTitle>
<AlertDescription>{description}</AlertDescription>
</Alert>
);
}

export default function NewScheduledTaskPage() {
const { t } = useI18n();
const router = useRouter();
const searchParams = useSearchParams();
const initialThreadId = searchParams.get("thread_id");
const st = t.scheduledTasks;
const createTask = useCreateScheduledTask();
const [contextMode, setContextMode] = useState<
"fresh_thread_per_run" | "reuse_thread"
>(initialThreadId ? "reuse_thread" : "fresh_thread_per_run");
const [targetThreadId, setTargetThreadId] = useState(initialThreadId ?? "");
const [title, setTitle] = useState("");
const [prompt, setPrompt] = useState("");
const [createSchedule, setCreateSchedule] = useState<ScheduleValue>({
schedule_type: "cron",
schedule_spec: { cron: "0 9 * * *" },
timezone: "",
});

const listHref = initialThreadId
? `/workspace/scheduled-tasks?thread_id=${encodeURIComponent(initialThreadId)}`
: "/workspace/scheduled-tasks";

const handleCreate = () => {
createTask.mutate(
{
context_mode: contextMode,
thread_id: contextMode === "reuse_thread" ? targetThreadId : null,
title,
prompt,
schedule_type: createSchedule.schedule_type,
schedule_spec: createSchedule.schedule_spec,
timezone: createSchedule.timezone || "UTC",
},
{ onSuccess: () => router.push(listHref) },

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Return to a list that can contain the created task

listHref is always derived from the inbound initialThreadId, but this form lets the user switch to fresh_thread_per_run or edit targetThreadId. In those cases the POST creates a task with thread_id: null or a different thread ID, then success navigates back to the original thread's filtered list, where the new task cannot appear. Keeping listHref for Back/Cancel makes sense, but the success destination should follow the submitted context (global for fresh tasks, or the chosen target for reuse tasks). The new round-trip test covers only the unchanged pre-seeded thread ID, so it misses both branches.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in bd73174. Back/Cancel still use inbound listHref. Create success now follows the submitted context: global list for fresh_thread_per_run, or ?thread_id= of the chosen targetThreadId for reuse_thread. Added e2e for switching to Fresh from a seeded filter, and for editing the reuse thread ID before submit.

);
};

return (
<div className="flex size-full flex-col">
<header className="flex shrink-0 items-center justify-between gap-3 border-b px-4 py-3">
<div className="flex items-center gap-3">
<Button
variant="ghost"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Give the back button an accessible name

This icon-only navigation button has no aria-label or other accessible name, so assistive technology announces it only as an unnamed button. Please add a localized label such as “Back to scheduled tasks” (and optionally a matching title).

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in d8da426. The icon-only back button now has a localized aria-label/title (st.create.back: "Back to scheduled tasks" / "返回定时任务").

size="icon-sm"
aria-label={st.create.back}
title={st.create.back}
onClick={() => router.push(listHref)}
>
<ArrowLeftIcon className="h-4 w-4" />
</Button>
<h1 className="text-sm font-semibold">{st.create.title}</h1>
</div>
</header>
<main className="flex flex-1 justify-center overflow-y-auto px-4 py-6">
<div
className="my-auto w-full max-w-xl space-y-4"
data-testid="scheduled-task-create-form"
>
<div className="space-y-3 pb-2 text-center">
<div className="bg-primary/10 mx-auto flex h-14 w-14 items-center justify-center rounded-full">
<CalendarClock className="text-primary h-7 w-7" />
</div>
<div className="space-y-1">
<h2 className="text-xl font-semibold">{st.create.title}</h2>
<p className="text-muted-foreground text-sm">{st.description}</p>
</div>
</div>
<div className="flex flex-wrap gap-2">
<Button
type="button"
variant={
contextMode === "fresh_thread_per_run" ? "default" : "outline"
}
size="sm"
onClick={() => setContextMode("fresh_thread_per_run")}
>
{st.context.fresh}
</Button>
<Button
type="button"
variant={contextMode === "reuse_thread" ? "default" : "outline"}
size="sm"
onClick={() => setContextMode("reuse_thread")}
>
{st.context.reuse}
</Button>
</div>
{contextMode === "reuse_thread" && (
<>
<Input
value={targetThreadId}
onChange={(event) => setTargetThreadId(event.target.value)}
placeholder={st.context.threadIdPlaceholder}
/>
<ReuseThreadNotice
title={st.context.reuseNoticeTitle}
description={st.context.reuseNoticeDescription}
/>
</>
)}
<Input
value={title}
onChange={(event) => setTitle(event.target.value)}
placeholder={st.create.taskTitle}
/>
<Textarea
rows={4}
value={prompt}
onChange={(event) => setPrompt(event.target.value)}
placeholder={st.create.prompt}
/>
<ScheduledTaskScheduleInput
initial={createSchedule}
onChange={setCreateSchedule}
/>
{createSchedule.schedule_type === "once" &&
!createSchedule.schedule_spec.run_at && (
<div className="text-muted-foreground text-sm">
{st.create.invalidOnce}
</div>
)}
<div className="flex justify-end gap-2">
<Button
variant="outline"
onClick={() => router.push(listHref)}
disabled={createTask.isPending}
>
{t.common.cancel}
</Button>
<Button
onClick={handleCreate}
disabled={
!title ||
!prompt ||
(!createSchedule.schedule_spec.cron &&
!createSchedule.schedule_spec.run_at) ||
(contextMode === "reuse_thread" && !targetThreadId) ||
createTask.isPending
}
>
{createTask.isPending ? t.common.loading : st.create.submit}
</Button>
</div>
</div>
</main>
</div>
);
}
Loading
Loading