From 6759f66a518ca920aa0cbd6006255efee9c17283 Mon Sep 17 00:00:00 2001 From: setkyar Date: Thu, 17 Sep 2026 01:29:15 +0700 Subject: [PATCH 1/2] feat(web): add Schedules nav to the session header MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Schedules were only reachable from the sessions index, so opening one from an active session meant leaving the conversation first. Put the same control next to + New (and in the session ⋯ menu) so /schedules is one click away. --- docs/architecture/frontend.md | 1 + e2e/tests/schedules.spec.ts | 13 +++++ internal/ui/embedded/styles/session.css | 47 +++++++++++++++++++ web/src/components/session/CommandMenu.svelte | 8 +++- .../components/session/CommandMenu.test.js | 13 +++++ .../components/session/SessionHeader.svelte | 20 +++++++- .../components/session/SessionHeader.test.js | 30 ++++++++++++ 7 files changed, 130 insertions(+), 2 deletions(-) create mode 100644 web/src/components/session/SessionHeader.test.js diff --git a/docs/architecture/frontend.md b/docs/architecture/frontend.md index 82aeb039..bf8274b9 100644 --- a/docs/architecture/frontend.md +++ b/docs/architecture/frontend.md @@ -30,6 +30,7 @@ Browser routes served by the SPA shell: - `/` → `web/src/routes/SessionsPage.svelte` - `/session?id=…` → `web/src/routes/SessionPage.svelte` +- `/schedules` → `web/src/routes/SchedulesPage.svelte` - `/settings` → `web/src/routes/SettingsPage.svelte` - `/login` → `web/src/routes/LoginPage.svelte` diff --git a/e2e/tests/schedules.spec.ts b/e2e/tests/schedules.spec.ts index 7260a9bc..12271c7a 100644 --- a/e2e/tests/schedules.spec.ts +++ b/e2e/tests/schedules.spec.ts @@ -46,6 +46,19 @@ test.describe("schedules (stubbed pi)", () => { expect(maxWidth).toBe("880px"); }); + test("session header button opens the schedules page", async ({ page }) => { + await page.goto("/"); + await page.locator("[data-sessions-content].index-layout-ready").waitFor(); + await page + .locator(".session-card", { hasText: "add deepseek-v4-pro" }) + .click(); + await expect(page).toHaveURL(/\/session\?id=/); + + await page.locator(".session-header-bar [data-schedules-btn]").click(); + await expect(page).toHaveURL(/\/schedules$/); + await expect(page.locator(".schedules-page")).toBeVisible(); + }); + test("create, run now, view run log, and delete a schedule", async ({ page, }, testInfo) => { diff --git a/internal/ui/embedded/styles/session.css b/internal/ui/embedded/styles/session.css index e2892975..b3ff2472 100644 --- a/internal/ui/embedded/styles/session.css +++ b/internal/ui/embedded/styles/session.css @@ -2608,6 +2608,53 @@ border-color: var(--pi-menu-border); } + .session-header-schedules { + height: 24px; + padding: 0 9px 0 7px; + font-size: 11px; + font-weight: 500; + font-family: inherit; + display: inline-flex; + align-items: center; + gap: 4px; + background: color-mix(in srgb, var(--surface) 54%, transparent); + color: var(--muted); + border: 1px solid color-mix(in srgb, var(--dim) 76%, transparent); + border-radius: 4px; + cursor: pointer; + flex-shrink: 0; + margin-right: 8px; + text-decoration: none; + transition: color 0.12s, background 0.12s, border-color 0.12s; + } + + .session-header-schedules-icon { + display: inline-flex; + align-items: center; + color: var(--accent); + } + + .session-header-schedules:hover, + .session-header-schedules:active { + color: var(--text); + background: var(--pi-menu-bg); + border-color: var(--pi-menu-border); + } + + @media (max-width: 900px) { + .session-header-schedules { + padding: 0; + width: 28px; + height: 28px; + justify-content: center; + gap: 0; + } + + .session-header-schedules-label { + display: none; + } + } + .session-header-new { height: 24px; padding: 0 9px 0 7px; diff --git a/web/src/components/session/CommandMenu.svelte b/web/src/components/session/CommandMenu.svelte index 33574fec..81bd09f1 100644 --- a/web/src/components/session/CommandMenu.svelte +++ b/web/src/components/session/CommandMenu.svelte @@ -19,12 +19,13 @@ ChartColumn, BookOpen, Send, + CalendarClock, Settings, Tag, } from '../../shared/icons.js'; import * as sidebarApi from '../../session/ui/sidebar.js'; import { openVersionModal } from '../../shared/version.js'; - import { navigate } from '../../shared/navigation.js'; + import { navigate, handleNavClick } from '../../shared/navigation.js'; import { openSessionPalette } from '../../shared/command-palette-runtime.js'; import { openModelUsage, openFork, openDiff } from '../../session/session-modals.svelte.js'; import { showToast } from '../../shared/toast.js'; @@ -68,6 +69,7 @@ icon: Send, label: 'common.telegram', }, + { kind: 'link', href: '/schedules', icon: CalendarClock, label: 'schedules.navTitle' }, { kind: 'link', href: '/settings', icon: Settings, label: 'common.settings', kbd: '⌘,' }, { kind: 'version', action: 'version', icon: Tag, label: 'common.version', desktopOnly: true }, ]; @@ -283,6 +285,10 @@ role="menuitem" target={item.external ? '_blank' : undefined} rel={item.external ? 'noreferrer' : undefined} + onclick={(event) => { + if (item.external) return; + handleNavClick(event, item.href); + }} >{@render label(item)}{#if desktop && item.kbd}{item.kbd}{/if} {:else if item.kind === 'version'} diff --git a/web/src/components/session/CommandMenu.test.js b/web/src/components/session/CommandMenu.test.js index a94c5031..3e7cdf87 100644 --- a/web/src/components/session/CommandMenu.test.js +++ b/web/src/components/session/CommandMenu.test.js @@ -61,6 +61,19 @@ describe('CommandMenu', () => { expect(sessionTitle.name).toBe('Old'); }); + it('includes a schedules link in the actions menu', async () => { + const pushState = vi.spyOn(window.history, 'pushState'); + render(CommandMenu, { props: { sessionId: 's' } }); + await tick(); + + const links = [...document.querySelectorAll('a[href="/schedules"]')]; + expect(links.length).toBeGreaterThan(0); + expect(links[0].textContent).toContain('Schedules'); + + await fireEvent.click(links[0]); + expect(pushState).toHaveBeenCalledWith({}, '', '/schedules'); + }); + it('opens model usage via the modal store + the session-list palette runtime', async () => { const openPalette = vi.fn(); render(CommandMenu, { props: { sessionId: 's' } }); diff --git a/web/src/components/session/SessionHeader.svelte b/web/src/components/session/SessionHeader.svelte index f88c374e..29f84e93 100644 --- a/web/src/components/session/SessionHeader.svelte +++ b/web/src/components/session/SessionHeader.svelte @@ -1,6 +1,13 @@