Skip to content
This repository was archived by the owner on Oct 21, 2026. It is now read-only.

Commit 22c337f

Browse files
lucas77778xiaoland
andauthored
feat(automations): align layout with Codex (#501)
* feat(automations): align layout with Codex * feat(automations): add safe task editing foundations * feat(automations): model schedule drafts and repeat presets * feat(automations): guard drafts and expose task actions * feat(automations): keep filtered task lists mounted * feat(automations): unify schedule creation and editing * feat(automations): simplify loop setup with progressive disclosure * fix(automations): preserve results and stabilize detail panels * fix(desktop): stop session chrome bleeding into automations overlay The persistent DesktopChrome never suppressed its right-rail panel toggles or session title/menu while the Automations overlay covers the workspace, so they kept showing on top of it. Null out rightControls and titleContent while automationsOpen, matching the existing !automationsOpen gate already used for the Resources control. * feat(automations): rework schedule/loop form fields, add weekday multi-select - Move Loop's agent/harness selector out of Advanced settings into the primary flow, alongside project selection. - Move Schedule's timezone field into Advanced settings now that it's confirmed load-bearing for cadence calculation (DST-aware next-run math), not cosmetic. - Convert the misfire policy control from a radio group to a dropdown. - Support selecting several weekdays for a weekly schedule: the form model now carries an array, the cadence builder joins them into a comma-separated cron field (croner already parses that), and the preset recognizer round-trips a multi-weekday cron expression back into the friendly weekly UI. Adds a new TaskMultiSelect checklist popover to back the picker. * feat(automations): add a resizable, layout-stable detail panel - New ResizeHandle (packages/presentation/ui): a standalone drag divider that reads no sibling DOM geometry and owns no styling contract, unlike the desktop shell's Sash — any panel can report a clamped size through onResize/onResizeEnd and decide how to render and persist it. Live drag frames stay local to the caller; only the settled size needs persisting. - New persisted useAutomationDetailWidthStore backing the Automations detail pane's width (420-960px, default 640). - automations-view.tsx: the list pane is now the fluid grid column and the detail pane is the fixed, user-resizable one, mirroring how the session view's own right panel works. Also adds a fixed top-right slot (DetailHeaderSlotProvider) next to the close button for detail views to portal contextual header actions into, and removes the list pane's masterDetailVisible-driven title/button/padding swap so expanding or collapsing the detail pane only animates the outer grid track — nothing inside the list has to reflow or pop. - Drop the max-w-xl cap on the create-form pane so its content fills the resizable width instead of re-centering at a fixed 576px. * fix(automations): unify detail-panel header actions and dialog padding - Schedule and Loop detail views now portal their AutomationActions overflow menu into the fixed slot next to the close button instead of rendering it inline in their own scrollable header — the two controls were only ever coincidentally adjacent, and scrolled apart as soon as the panel's content moved. - Split Schedule's run history out of the disclosure that used to sit after the Save/Cancel row into its own "Run history" tab, alongside an "Edit" tab holding the existing form — nothing renders below Save now. - Remove the duplicate heading text repeated inside the Loop log and Schedule run-history disclosures (the trigger already shows it). - Give the discard-unsaved-changes and delete-confirmation dialogs their missing padding via AlertDialogHeader/AlertDialogFooter, matching every other dialog in the codebase. - Drop the now-unused "title" string; the in-page <h1> it backed was removed together with the list pane's other content that swapped on selection. * fix(automations): center-align the status badge on wide list rows The status badge sat inline with the truncated name (top-aligned with the rest of the row), while the sibling "more actions" button is vertically centered against the row's full height — visibly offset once the list pane is wide enough for a two-line row to have real height to spare. A container query on the row now moves the badge to a trailing, self-centered column past 380px of row width, matching the actions button; narrower rows keep the existing inline placement. --------- Co-authored-by: Lan_zhijiang <lanzhijiang@foxmail.com>
1 parent 972a40c commit 22c337f

49 files changed

Lines changed: 2517 additions & 883 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/desktop/src/renderer/src/__tests__/pane-transition.test.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { SplitTransitionState } from '@renderer/shell/layout/pane-transition';
2-
import { reconcileTransition, settleTransition } from '@renderer/shell/layout/pane-transition';
1+
import type { SplitTransitionState } from '@linkcode/ui';
2+
import { reconcileTransition, settleTransition } from '@linkcode/ui';
33
import { describe, expect, it } from 'vitest';
44

55
function transition(overrides?: Partial<SplitTransitionState>): SplitTransitionState {

‎apps/desktop/src/renderer/src/app.tsx‎

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
import { useEffect } from 'foxact/use-abortable-effect';
1414
import { useState } from 'react';
1515
import useSWRImmutable from 'swr/immutable';
16-
import { DesktopAutomationsView } from './automations/automations-view';
1716
import { cloudDataBridge } from './cloud-auth/bridges';
1817
import { desktopDaemonConnectionSource } from './daemon-connection-source';
1918
import { systemBridge } from './ipc';
@@ -36,7 +35,6 @@ const { allowedAgents } = systemBridge.identity.restrictions();
3635
export function DesktopApp(): React.ReactNode {
3736
const localeOverride = useDesktopSettingsStore((state) => state.localeOverride);
3837
const settingsOpen = useNavigationHistoryStore((state) => state.overlay === 'settings');
39-
const automationsOpen = useNavigationHistoryStore((state) => state.overlay === 'automations');
4038

4139
return (
4240
<WorkbenchAppProviders locale={localeOverride}>
@@ -53,8 +51,6 @@ export function DesktopApp(): React.ReactNode {
5351
<OverlayUnderlay>
5452
<Workbench shellComponent={DesktopWorkbenchShell} allowedAgents={allowedAgents} />
5553
</OverlayUnderlay>
56-
{/* Gated: Automations lists schedules over the data plane, so it mounts inside the gate. */}
57-
{automationsOpen ? <DesktopAutomationsView /> : null}
5854
</WorkbenchProviders>
5955
{/* Window controls live above the connection gate and the settings overlay so Windows/Linux
6056
can always minimize/maximize/close — including while the daemon is connecting or down. */}
@@ -66,12 +62,11 @@ export function DesktopApp(): React.ReactNode {
6662
}
6763

6864
/**
69-
* Hides (never unmounts) the workbench-side layer while a full-page overlay (Settings, Automations)
70-
* covers it: both shells are translucent over the native backdrop, so painted pixels underneath
71-
* ghost through the overlay. `visibility` keeps layout/PTY state intact; `inert` blocks focus.
65+
* Hides (never unmounts) the workbench-side layer while Settings covers it. Automations stays
66+
* inside the shell so its app sidebar remains visible.
7267
*/
7368
function OverlayUnderlay({ children }: React.PropsWithChildren): React.ReactNode {
74-
const overlayOpen = useNavigationHistoryStore((state) => state.overlay !== null);
69+
const overlayOpen = useNavigationHistoryStore((state) => state.overlay === 'settings');
7570
return (
7671
<div className={overlayOpen ? 'invisible h-full' : 'h-full'} inert={overlayOpen}>
7772
{children}

‎apps/desktop/src/renderer/src/automations/automations-view.tsx‎

Lines changed: 0 additions & 43 deletions
This file was deleted.

‎apps/desktop/src/renderer/src/index.css‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@
127127
* subtree every frame.) DesktopShell sets one axis-specific animation attribute only while
128128
* that axis is in flight, so sash drags and resting window resizes keep tracking live.
129129
* Row and column durations are independent so a bottom toggle cannot animate unrelated chrome
130-
* geometry. Keep duration/bezier in sync with SHELL_TRANSITION in pane-transition.ts.
130+
* geometry. Keep duration/bezier in sync with @linkcode/ui's SHELL_TRANSITION.
131131
*/
132132
.linkcode-desktop-shell[data-shell-horizontal-animating] {
133133
--lc-shell-horizontal-duration: 300ms;

‎apps/desktop/src/renderer/src/shell/desktop-shell.tsx‎

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
SessionSidebar,
1111
SessionTitleMenu,
1212
useKeyboardShortcutLabel,
13+
usePaneTransition,
1314
} from '@linkcode/ui';
1415
import {
1516
getChromeSurface,
@@ -19,6 +20,7 @@ import {
1920
import type { WorkbenchShellProps } from '@linkcode/workbench';
2021
import {
2122
AttachedTerminalPanel,
23+
AutomationsView,
2224
getResourcesPanelPresentation,
2325
isAbsoluteFilePath,
2426
locateFileArtifact,
@@ -29,6 +31,7 @@ import {
2931
TerminalPanel,
3032
useBrowserHostRegistration,
3133
useCloudHosts,
34+
useNavigationHistoryStore,
3235
useResourcesPanelStore,
3336
useSelectedHostStore,
3437
WorkspaceServicesMenu,
@@ -50,7 +53,6 @@ import { BrowserWebviewPane } from './browser/browser-webview-pane';
5053
import { DesktopChrome } from './chrome/chrome';
5154
import { DiffStatChip } from './chrome/diff-stat-chip';
5255
import { DESKTOP_CHROME_SPACER_CLASS } from './chrome/metrics';
53-
import { usePaneTransition } from './layout/pane-transition';
5456
import { DesktopPanelRegion } from './layout/panel-region';
5557
import { DesktopRightPanelRegion } from './layout/right-panel-region';
5658
import type { DesktopShellStyle } from './layout/shell-style';
@@ -172,6 +174,7 @@ export function DesktopShell({
172174
})),
173175
);
174176
const cloudAuth = useCloudAccount();
177+
const automationsOpen = useNavigationHistoryStore((state) => state.overlay === 'automations');
175178
const resourcesOpen = useResourcesPanelStore((state) => state.open);
176179
const setResourcesOpen = useResourcesPanelStore((state) => state.setOpen);
177180
const toggleResources = useResourcesPanelStore((state) => state.toggle);
@@ -297,13 +300,15 @@ export function DesktopShell({
297300

298301
const active = activeSession;
299302
const activeSessionId = active?.sessionId ?? null;
300-
const resourcesAvailable = draft === null && active !== null && resourcesPanel !== undefined;
303+
const resourcesAvailable =
304+
!automationsOpen && draft === null && active !== null && resourcesPanel !== undefined;
301305
const resourcesPresentation = getResourcesPanelPresentation({
302306
available: resourcesAvailable,
303307
floatingSpaceAvailable,
304308
rightPanelOpen: rightPanel.open,
305309
});
306-
const resourcesFloatingOpen = resourcesPresentation === 'floating' && resourcesOpen;
310+
const resourcesFloatingOpen =
311+
!automationsOpen && resourcesPresentation === 'floating' && resourcesOpen;
307312
const resourcesSurfaceOpen =
308313
(resourcesPresentation !== 'hidden' && resourcesOpen) ||
309314
(rightPanel.open && rightPanel.activeSection === 'resources');
@@ -678,8 +683,11 @@ export function DesktopShell({
678683
sidebarShortcut={sidebarShortcut}
679684
rightPanelShortcut={rightPanelShortcut}
680685
bottomPanelShortcut={bottomPanelShortcut}
686+
// Automations is a full-page overlay; suppress the session title/chip/menu and the
687+
// right-rail panel toggles the same way settings-view.tsx does for its own overlay.
688+
rightControls={automationsOpen ? null : undefined}
681689
titleContent={
682-
hideMainTitle ? (
690+
automationsOpen ? null : hideMainTitle ? (
683691
// An untitled conversation hides the title area, which would also hide the error
684692
// badge with no banner fallback; keep the badge alone. The draft page stays bare —
685693
// it reports errors through its own in-page banner.
@@ -747,9 +755,19 @@ export function DesktopShell({
747755
>
748756
{/* Never paint this wrapper: it spans the sidebar column, whose translucent tint has to
749757
reach the native backdrop. Only the floating rail needs its own opaque gutter. */}
750-
<div className="grid h-full min-h-0 grid-cols-[minmax(0,1fr)_auto] overflow-hidden">
758+
<div className="grid h-full min-h-0 grid-cols-[minmax(0,1fr)_auto] overflow-clip">
751759
<DesktopWorkspace
752760
main={main}
761+
workspaceOverlay={
762+
automationsOpen ? (
763+
<div className="flex h-full min-h-0 flex-col bg-background">
764+
<div aria-hidden className={`${DESKTOP_CHROME_SPACER_CLASS} shrink-0`} />
765+
<div className="min-h-0 flex-1">
766+
<AutomationsView onOpenSession={onSelectSession} />
767+
</div>
768+
</div>
769+
) : undefined
770+
}
753771
right={workspaceRight}
754772
bottom={workspaceBottom}
755773
expandedPanel={expandedPanel}
@@ -795,6 +813,7 @@ export function DesktopShell({
795813
onPickDirectory={pickDirectory}
796814
onOpenSearch={onOpenSearch}
797815
onOpenAutomations={onOpenAutomations}
816+
automationsActive={automationsOpen}
798817
searchShortcut={searchShortcut}
799818
onRegisterWorkspace={onRegisterWorkspace}
800819
onImportHistory={onImportHistory}

‎apps/desktop/src/renderer/src/shell/layout/workspace.tsx‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import type { PaneTransition } from '@linkcode/ui';
12
import { cn } from '@linkcode/ui';
23
import type { LayoutState, PanelSide } from '@renderer/shell/store/model';
34
import {
@@ -11,7 +12,6 @@ import {
1112
SIDEBAR_MIN_SIZE,
1213
} from '@renderer/shell/store/model';
1314
import { useTranslations } from 'use-intl';
14-
import type { PaneTransition } from './pane-transition';
1515
import { Sash } from './sash';
1616

1717
/** One dockable side of the workspace: its transition plus the docked and maximized-overlay nodes. */
@@ -44,6 +44,7 @@ const PANE_ID = {
4444
export function DesktopWorkspace({
4545
sidebar,
4646
main,
47+
workspaceOverlay,
4748
right,
4849
bottom,
4950
expandedPanel,
@@ -55,6 +56,8 @@ export function DesktopWorkspace({
5556
}: {
5657
sidebar: WorkspaceSidebar;
5758
main: React.ReactNode;
59+
/** Covers every workspace pane except the persistent app sidebar. */
60+
workspaceOverlay?: React.ReactNode;
5861
right: WorkspaceSide;
5962
bottom: WorkspaceSide;
6063
expandedPanel: PanelSide | null;
@@ -66,11 +69,12 @@ export function DesktopWorkspace({
6669
onBottomResize: (size: number) => void;
6770
}): React.ReactNode {
6871
const tPanel = useTranslations('workbench.panel');
72+
const workspaceOverlayOpen = workspaceOverlay !== undefined;
6973
const rowOverlayPanel = getExpandedPanelForTarget(expandedPanel, 'editor-row');
7074
const workbenchOverlayPanel = getExpandedPanelForTarget(expandedPanel, 'workbench');
7175
// Expanded panels render as direct overlays. Docked panels stay mounted so
7276
// they keep owning chrome portals and panel state.
73-
const dockedInert = workbenchOverlayPanel !== null;
77+
const dockedInert = workbenchOverlayPanel !== null || workspaceOverlayOpen;
7478
const editorInert = rowOverlayPanel !== null || dockedInert;
7579

7680
const anyAnimating =
@@ -290,6 +294,11 @@ export function DesktopWorkspace({
290294
{workbenchOverlayPanel === 'right' ? right.expandedNode : bottom.expandedNode}
291295
</ExpandedPanelOverlay>
292296
)}
297+
{workspaceOverlayOpen ? (
298+
<div className="z-40 col-end-4 col-start-2 row-span-2 row-start-1 min-h-0 min-w-0 overflow-hidden bg-background">
299+
{workspaceOverlay}
300+
</div>
301+
) : null}
293302
</div>
294303
);
295304
}

‎apps/webview/src/router.tsx‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { AutomationsRoute } from '@webview/routes/automations';
21
import { RootLayout } from '@webview/routes/root-layout';
32
import { AgentsSettings } from '@webview/routes/settings/agents';
43
import { AppearanceSettings } from '@webview/routes/settings/appearance';
@@ -22,8 +21,10 @@ export function createWebviewRouter(
2221
{
2322
element: <RootLayout />,
2423
children: [
25-
{ index: true, element: <WorkbenchRoute /> },
26-
{ path: 'automations', element: <AutomationsRoute /> },
24+
{
25+
element: <WorkbenchRoute />,
26+
children: [{ index: true }, { path: 'automations' }],
27+
},
2728
{
2829
path: 'settings',
2930
element: <SettingsLayout />,

‎apps/webview/src/routes/automations.tsx‎

Lines changed: 0 additions & 34 deletions
This file was deleted.

‎apps/webview/src/routes/workbench-route.tsx‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,19 @@ import { useCommandPaletteStore, Workbench } from '@linkcode/workbench';
22
import { usePageTitle } from '@webview/hooks/use-page-title';
33
import { WebWorkbenchShell } from '@webview/shell/web-workbench-shell';
44
import { useEffect } from 'react';
5-
import { useNavigate } from 'react-router';
5+
import { useLocation, useNavigate } from 'react-router';
66
import { useTranslations } from 'use-intl';
77

88
/** Index route: the workbench surface (session / conversation / composer). */
99
export function WorkbenchRoute(): React.ReactNode {
1010
const navigate = useNavigate();
11+
const location = useLocation();
1112
const t = useTranslations('workbench.palette');
1213
const tWorkbench = useTranslations('workbench');
13-
usePageTitle(tWorkbench('pageTitle'));
14+
const tAutomations = useTranslations('workbench.automations');
15+
usePageTitle(
16+
location.pathname === '/automations' ? tAutomations('pageTitle') : tWorkbench('pageTitle'),
17+
);
1418
useEffect(() => {
1519
const { registerCommands, unregisterCommands } = useCommandPaletteStore.getState();
1620
registerCommands('webview', [

0 commit comments

Comments
 (0)