feat(desktop): cron + blueprint recipes in the sidebar nav rail

Adds a 'Scheduled jobs' row to the sidebar's top nav (below Artifacts,
watch codicon, wired to the existing nav.cron keybind action) so the
cron overlay opens from the same rail as New session / Capabilities /
Messaging / Artifacts.

Inside the cron overlay, the list rail now also shows the Automation
Blueprint recipes below the jobs (same search box filters both).
Clicking a recipe opens the create dialog pre-seeded to that
blueprint's typed-slot form via a new optional blueprintKey on the
create EditorState. Catalog fetch reuses the ['cron-blueprints'] query
key, so no extra request.

i18n: sidebar.nav.cron added to en/zh/zh-hant/ja (ar already had it).
This commit is contained in:
Teknium 2026-08-13 01:52:45 -07:00
parent 7fa084f58e
commit 926c8d591a
7 changed files with 59 additions and 9 deletions

View File

@ -127,6 +127,7 @@ import { $sidebarSessionRankIds } from '@/store/sidebar-sort'
import {
type AppView,
ARTIFACTS_ROUTE,
CRON_ROUTE,
MESSAGING_ROUTE,
SIDEBAR_NAV_AREA,
type SidebarNavContribution,
@ -203,6 +204,13 @@ const SIDEBAR_NAV: SidebarNavItem[] = [
icon: props => <Codicon name="files" {...props} />,
route: ARTIFACTS_ROUTE,
keybindActionId: 'nav.artifacts'
},
{
id: 'cron',
label: '',
icon: props => <Codicon name="watch" {...props} />,
route: CRON_ROUTE,
keybindActionId: 'nav.cron'
}
]
@ -1405,6 +1413,7 @@ export function ChatSidebar({
(item.id === 'skills' && currentView === 'skills') ||
(item.id === 'messaging' && currentView === 'messaging') ||
(item.id === 'artifacts' && currentView === 'artifacts') ||
(item.id === 'cron' && currentView === 'cron') ||
// Contributed rows light up at their own route.
(Boolean(item.route) && pathname === item.route)

View File

@ -356,6 +356,23 @@ export function CronView({ onClose, onOpenSession, setStatusbarItemGroup: _setSt
[jobs, query]
)
// Blueprint recipes render in the same list rail, below the jobs — clicking
// one opens the create dialog pre-seeded to that recipe. Same query key as
// the dialog's "Start from" dropdown, so the catalog is fetched once.
const blueprintsQuery = useQuery({
queryKey: ['cron-blueprints'],
queryFn: async () => (await getAutomationBlueprints()).blueprints
})
const visibleBlueprints = useMemo(() => {
const list = blueprintsQuery.data ?? []
const needle = query.trim().toLowerCase()
return needle
? list.filter(item => `${item.title} ${item.description}`.toLowerCase().includes(needle))
: list
}, [blueprintsQuery.data, query])
// Detail always reflects a concrete job: the explicitly selected one, else the
// first visible row, so the right pane is never empty while jobs exist.
const selectedJob = useMemo(
@ -480,7 +497,7 @@ export function CronView({ onClose, onOpenSession, setStatusbarItemGroup: _setSt
{loading && jobs.length === 0 ? (
<PageLoader label={c.loading} />
) : totalCount === 0 ? (
) : totalCount === 0 && visibleBlueprints.length === 0 ? (
<PanelEmpty
action={
<Button onClick={() => setEditor({ mode: 'create' })} size="sm">
@ -521,6 +538,21 @@ export function CronView({ onClose, onOpenSession, setStatusbarItemGroup: _setSt
<p className="px-2 py-4 text-center text-xs text-muted-foreground">{c.emptyTitleSearch}</p>
)}
<PanelAddButton label={c.newCron} onClick={() => setEditor({ mode: 'create' })} />
{visibleBlueprints.length > 0 && (
<>
<PanelSectionLabel className="mt-3 px-2">{c.blueprints.tab}</PanelSectionLabel>
{visibleBlueprints.map(item => (
<PanelListRow
active={false}
icon="rocket"
key={item.key}
onSelect={() => setEditor({ blueprintKey: item.key, mode: 'create' })}
rowKey={`blueprint-${item.key}`}
title={item.title}
/>
))}
</>
)}
</PanelList>
{selectedJob ? (
@ -919,10 +951,10 @@ function CronEditorDialog({
setDeliver(initial ? jobDeliver(initial) : DEFAULT_DELIVER)
setModelChoice(initial && jobModel(initial) ? `${jobProvider(initial)}:${jobModel(initial)}` : MODEL_DEFAULT_VALUE)
setSlotValues({})
setTemplateChoice(CUSTOM_TEMPLATE)
setTemplateChoice(editor.mode === 'create' ? (editor.blueprintKey ?? CUSTOM_TEMPLATE) : CUSTOM_TEMPLATE)
setError(null)
setSaving(false)
}, [initial, open])
}, [editor, initial, open])
// Seed the typed slots with the blueprint's defaults whenever a blueprint is
// picked from "Start from" (and reset them when switching back to Custom).
@ -1232,7 +1264,12 @@ function CronEditorDialog({
)
}
type EditorState = { job: CronJob; mode: 'edit' } | { mode: 'closed' } | { mode: 'create' }
type EditorState =
| { job: CronJob; mode: 'edit' }
| { mode: 'closed' }
// `blueprintKey` pre-selects a blueprint in the create dialog's "Start from"
// dropdown (set when a recipe row in the list rail is clicked).
| { blueprintKey?: string; mode: 'create' }
interface EditorValues {
deliver: string

View File

@ -158,7 +158,7 @@ export type CommandDispatchResponse =
| SendCommandDispatchResponse
| PrefillCommandDispatchResponse
export type SidebarNavId = 'artifacts' | 'command-center' | 'messaging' | 'new-session' | 'settings' | 'skills'
export type SidebarNavId = 'artifacts' | 'command-center' | 'cron' | 'messaging' | 'new-session' | 'settings' | 'skills'
export interface SidebarNavItem {
/** Built-in view id, or a contributed row's namespaced contribution id. */

View File

@ -1865,7 +1865,8 @@ export const en: Translations = {
'new-session': 'New session',
skills: 'Capabilities',
messaging: 'Messaging',
artifacts: 'Artifacts'
artifacts: 'Artifacts',
cron: 'Scheduled jobs'
},
searchAria: 'Search sessions',
searchPlaceholder: 'Search sessions…',

View File

@ -1689,7 +1689,8 @@ export const ja = defineLocale({
'new-session': '新しいセッション',
skills: 'スキルとツール',
messaging: 'メッセージング',
artifacts: 'アーティファクト'
artifacts: 'アーティファクト',
cron: 'スケジュール済みジョブ'
},
searchAria: 'セッションを検索',
searchPlaceholder: 'セッションを検索…',

View File

@ -1634,7 +1634,8 @@ export const zhHant = defineLocale({
'new-session': '新工作階段',
skills: '技能與工具',
messaging: '訊息平台',
artifacts: '成品'
artifacts: '成品',
cron: '排程工作'
},
searchAria: '搜尋工作階段',
searchPlaceholder: '搜尋工作階段…',

View File

@ -2056,7 +2056,8 @@ export const zh: Translations = {
'new-session': '新建会话',
skills: '技能与工具',
messaging: '消息平台',
artifacts: '产物'
artifacts: '产物',
cron: '定时任务'
},
searchAria: '搜索会话',
searchPlaceholder: '搜索会话…',