Skip to content
Open
Show file tree
Hide file tree
Changes from 33 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
208 changes: 208 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,208 @@
"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 initialTitle = searchParams.get("title") ?? "";
const initialPrompt = searchParams.get("prompt") ?? "";
const initialContextMode = searchParams.get("context_mode");
const initialScheduleType = searchParams.get("schedule_type");
const initialCron = searchParams.get("cron");
const initialRunAt = searchParams.get("run_at");
const initialTimezone = searchParams.get("timezone");
const st = t.scheduledTasks;
const createTask = useCreateScheduledTask();
const [contextMode, setContextMode] = useState<
"fresh_thread_per_run" | "reuse_thread"
>(
initialContextMode === "reuse_thread" || initialThreadId

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.

[P1] Preserve explicit fresh-thread mode

The duplicate route can provide context_mode=fresh_thread_per_run together with a stored thread_id, but this condition converts that combination to reuse_thread. Submitting the duplicate then changes it from isolated runs to reusing an existing conversation. Give an explicit context_mode precedence and infer reuse from thread_id only when the mode parameter is absent.

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.

Addressed both P1s:

  • Duplicate no longer serializes the prompt (or other task contents) into the query string. It now navigates with ?from=<taskId> and hydrates the create form from sessionStorage, falling back to the scheduled-tasks list cache/API.
  • Explicit context_mode takes precedence over thread_id. Duplicating a fresh_thread_per_run task that still has a stored thread_id stays on isolated runs instead of being coerced to reuse_thread.

Also merged origin/main (copy-data cache + sandbox timeout) so this PR is no longer DIRTY.

? "reuse_thread"
: "fresh_thread_per_run",
);
const [targetThreadId, setTargetThreadId] = useState(initialThreadId ?? "");
const [title, setTitle] = useState(initialTitle);
const [prompt, setPrompt] = useState(initialPrompt);
const [createSchedule, setCreateSchedule] = useState<ScheduleValue>(() => {
if (initialScheduleType === "once") {
return {
schedule_type: "once",
schedule_spec: initialRunAt ? { run_at: initialRunAt } : {},
timezone: initialTimezone ?? "",
};
}
return {
schedule_type: "cron",
schedule_spec: { cron: initialCron ?? "0 9 * * *" },
timezone: initialTimezone ?? "",
};
});

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

const successHref =
contextMode === "reuse_thread" && targetThreadId
? `/workspace/scheduled-tasks?thread_id=${encodeURIComponent(targetThreadId)}`
: "/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(successHref) },
);
};

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
autoFocus
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