Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
385160e
feat(app): add sidebar sort label and short option keys
Astro-Han May 7, 2026
19c4dba
feat(app): extract sidebar 4-state status kind helper
Astro-Han May 7, 2026
b4d041d
feat(app): rewrite sidebar session row to L35 (h32, 4-state status)
Astro-Han May 7, 2026
ca97f6e
feat(app): add shortcut hint field to SessionMenuAction
Astro-Han May 7, 2026
71744cb
feat(app): rebuild sidebar shell with sort popover and four-segment l…
Astro-Han May 7, 2026
e5cced0
style(app): scope sidebar row iconbtn to L35 hover (radius-md + 0.06 …
Astro-Han May 7, 2026
3d84985
test(app): e2e for slice 09 sidebar shell + align sort selector
Astro-Han May 7, 2026
b87c4de
fix(app): use literal px for slice 09 row + status geometry
Astro-Han May 7, 2026
b973f3d
Merge remote-tracking branch 'origin/dev' into claude/slice-09-sidebar
Astro-Han May 7, 2026
1a2eba6
fix(app): drop dead Icon size prop after slice 08 collapse
Astro-Han May 7, 2026
efcb22e
fix(app): drop side-traffic placeholder until slice 17 owns the chrome
Astro-Han May 7, 2026
01e3fe9
fix(app): unify sidebar quick-action buttons under TooltipKeybind
Astro-Han May 7, 2026
81b37b3
fix(app): keep sidebar status time text on a single line
Astro-Han May 7, 2026
3ffa5a9
fix(app): unify sidebar action buttons to 32px nav-row height
Astro-Han May 7, 2026
a66376d
style(app): apply --duration-fast to sidebar row transition per L35
Astro-Han May 7, 2026
45c81be
fix(app): use 13px caption + h24 sort trigger per L35 typography lock
Astro-Han May 7, 2026
6cd9563
fix(app): lock sidebar section header containers to 32px nav-row
Astro-Han May 7, 2026
348b7b1
fix(app): apply affinity spacing to sidebar section headers
Astro-Han May 7, 2026
726829c
fix(app): always apply mt-4 to top-level sidebar section headers
Astro-Han May 7, 2026
5ac1404
fix(app): drop sidebar footer divider in favor of affinity spacing
Astro-Han May 7, 2026
397a678
feat(ui): lift primitive default heights to 32
Astro-Han May 7, 2026
d8ecc63
fix(app): include question and blocker in sidebar asking signal
Astro-Han May 7, 2026
18a6cc8
style(ui): replace pulse-block spinner with rotating ring
Astro-Han May 7, 2026
85cd042
style(ui): align spinner with STANDARDS keyshape and pw-spin keyframe
Astro-Han May 7, 2026
54f9e1e
style(ui): slow spinner from 700ms to 1200ms
Astro-Han May 7, 2026
5b138a0
style(app): lock sidebar status opacity to --duration-base 120ms
Astro-Han May 7, 2026
2717755
style(ui): lock dropdown menu items to 32px row family
Astro-Han May 7, 2026
bf2f1d6
refactor(app): swap session menu shortcut hints for leading icons
Astro-Han May 7, 2026
d8584d8
refactor(app): give sidebar sort options leading icons + check mark
Astro-Han May 7, 2026
fa9dcb0
refactor(app): switch sidebar sort trigger to icon-only IconButton
Astro-Han May 7, 2026
fd91386
feat(ui): add sort icon and wire it into sidebar trigger
Astro-Han May 7, 2026
0f0ab0e
fix(ui): align sort trigger hover and icon size with row-menu
Astro-Han May 7, 2026
7933607
revert(ui): restore sort trigger and icon to L21/L23 spec
Astro-Han May 7, 2026
2b89c50
feat(ui): unify IconButton hover to radius-md + 6% globally
Astro-Han May 7, 2026
b6e2e1a
fix(app): align all sidebar inner padding to 10px to match row spec
Astro-Han May 7, 2026
909e667
feat(app): add pin as 4th sidebar status priority (above time)
Astro-Han May 7, 2026
3797819
revert(app): drop pin icon from sidebar right-slot, restore 4-state
Astro-Han May 8, 2026
5b7ffcb
refactor(ui): merge dropdown-menu and context-menu css into shared me…
Astro-Han May 8, 2026
bbede09
fix(app): close settings overlay when navigating to a session from si…
Astro-Han May 8, 2026
944955f
fix(app): shorten settings footer en label to "PawWork"
Astro-Han May 8, 2026
ff2cda6
refactor(ui,app): polish command palette per L32 with fixed-height body
Astro-Han May 8, 2026
87afcf1
fix(app): unify composer picker trigger font weight to regular
Astro-Han May 8, 2026
78798cb
feat(app): collapsible project groups in sidebar
Astro-Han May 8, 2026
50e2b5b
test(app): cover project collapse and settings-close-on-nav with e2e
Astro-Han May 8, 2026
f06f27e
test(app): rename slice-09 sort spec and tighten shape assertions
Astro-Han May 8, 2026
eb05efa
fix(ui): scope IconButton row-overlay tokens to sidebar row trigger
Astro-Han May 8, 2026
f8f1554
fix(ui,app): localize Spinner aria-label and honour reduced motion
Astro-Han May 8, 2026
c47c9f9
fix(app): close settings overlay for non-shell navigation
Astro-Han May 8, 2026
f83227d
test(app): assert icon order in 3-state session menu case
Astro-Han May 8, 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
26 changes: 26 additions & 0 deletions packages/app/e2e/settings/settings-close-on-nav.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { test, expect } from "../fixtures"
import { openSettings, openSidebar, withSession } from "../actions"
import { pawworkSidebarSelector } from "../selectors"

test("clicking a sidebar session closes the settings overlay and navigates", async ({
page,
sdk,
gotoSession,
}) => {
const stamp = Date.now()
await withSession(sdk, `settings-nav a ${stamp}`, async (a) => {
await withSession(sdk, `settings-nav b ${stamp}`, async (b) => {
await gotoSession(a.id)
await openSidebar(page)

const settings = await openSettings(page)
await expect(settings).toBeVisible()

const sidebar = page.locator(pawworkSidebarSelector).first()
await sidebar.locator(`[data-session-id="${b.id}"]`).first().click()

await expect(settings).toBeHidden()
await expect.poll(() => page.url()).toContain(b.id)
})
})
})
9 changes: 2 additions & 7 deletions packages/app/e2e/sidebar/sidebar-leading-slot.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,8 @@ test("pawwork sidebar merges pin into the status slot with a stable title baseli
await pinnedRow.hover()
expect(await leftOf(pinnedTitle)).toBe(baseline)

// Pin button should occupy the row's leading slot, not a separate column.
const pinButton = pinnedRow.locator('[data-action="pawwork-session-pin"][data-pinned="true"]').first()
await expect(pinButton).toBeVisible()
const pinRect = await pinButton.evaluate((el) => el.getBoundingClientRect())
const rowRect = await pinnedRow.evaluate((el) => el.getBoundingClientRect())
expect(Math.round(pinRect.left - rowRect.left)).toBe(8)
expect(Math.round(pinRect.width)).toBe(24)
// Per L35 the pin glyph is removed from the row entirely; pin status is
// signaled by the row's presence in the Pinned section (asserted above).
})
})

Expand Down
66 changes: 66 additions & 0 deletions packages/app/e2e/sidebar/sidebar-project-collapse.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { test, expect } from "../fixtures"
import { openSidebar, withSession } from "../actions"
import { pawworkSidebarSelector } from "../selectors"

test("project group folds and unfolds", async ({ page, sdk, gotoSession }) => {
const stamp = Date.now()
await withSession(sdk, `collapse a ${stamp}`, async (a) => {
await withSession(sdk, `collapse b ${stamp}`, async () => {
await gotoSession(a.id)
await openSidebar(page)

const sidebar = page.locator(pawworkSidebarSelector).first()

// Switch to project sort so group headers exist.
await sidebar.locator('[data-action="pawwork-sort-trigger"]').click()
await page.locator('[data-action="pawwork-sort-option"][data-value="project"]').click()

const header = sidebar.locator('[data-action="pawwork-group-toggle"]').first()
const content = sidebar.locator('[data-component="pawwork-group-content"]').first()
await expect(header).toBeVisible()
await expect(header).toHaveAttribute("aria-expanded", "true")
await expect(content).not.toHaveAttribute("data-collapsed", "true")

await header.click()
await expect(header).toHaveAttribute("aria-expanded", "false")
await expect(header).toHaveAttribute("data-collapsed", "true")
// Items stay mounted (focus / scroll preserved); the wrapper is the hide signal.
await expect(content).toHaveAttribute("data-collapsed", "true")
await expect(content).toHaveAttribute("aria-hidden", "true")
// Visible height collapses to 0 via grid-template-rows: 0fr.
await expect.poll(async () => (await content.boundingBox())?.height ?? -1).toBeLessThan(2)

await header.click()
await expect(header).toHaveAttribute("aria-expanded", "true")
await expect(content).not.toHaveAttribute("data-collapsed", "true")
await expect.poll(async () => (await content.boundingBox())?.height ?? 0).toBeGreaterThan(0)
})
})
})

test("project collapsed state persists across reload", async ({ page, sdk, gotoSession }) => {
const stamp = Date.now()
await withSession(sdk, `persist collapse ${stamp}`, async (s) => {
await gotoSession(s.id)
await openSidebar(page)

const sidebar = page.locator(pawworkSidebarSelector).first()
await sidebar.locator('[data-action="pawwork-sort-trigger"]').click()
await page.locator('[data-action="pawwork-sort-option"][data-value="project"]').click()

const header = sidebar.locator('[data-action="pawwork-group-toggle"]').first()
await header.click()
await expect(header).toHaveAttribute("data-collapsed", "true")

await page.reload()
await openSidebar(page)
const reloaded = page
.locator(pawworkSidebarSelector)
.first()
.locator('[data-action="pawwork-group-toggle"]')
.first()
await expect(reloaded).toBeVisible()
await expect(reloaded).toHaveAttribute("data-collapsed", "true")
await expect(reloaded).toHaveAttribute("aria-expanded", "false")
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ test("users can pin, rename, and regroup sessions in the PawWork sidebar", async
await expect(dialog).toBeHidden()
await expect(sidebar.locator(`[data-session-id="${one.id}"]`)).toContainText(`Ops weekly renamed ${stamp}`)

await sidebar.locator('[data-action="pawwork-sort-mode"]').click()
await sidebar.locator('[data-action="pawwork-sort-trigger"]').click()
await page.locator('[data-action="pawwork-sort-option"][data-value="project"]').click()
await expect(sidebar.locator('[data-component="pawwork-group-header"]')).toHaveCount(1)
})
91 changes: 91 additions & 0 deletions packages/app/e2e/sidebar/sidebar-slice-09.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { test, expect } from "../fixtures"
import { openSidebar, withSession } from "../actions"
import { pawworkSidebarSelector } from "../selectors"

test("sidebar row exposes 4-item menu with leading icons", async ({
page,
sdk,
gotoSession,
}) => {
const stamp = Date.now()
await withSession(sdk, `slice 09 menu ${stamp}`, async (session) => {
await gotoSession(session.id)
await openSidebar(page)

const sidebar = page.locator(pawworkSidebarSelector).first()
const row = sidebar.locator(`[data-session-id="${session.id}"]`).first()
await row.hover()
await row.locator('[data-action="session-row-menu"]').click()

const items = page.getByRole("menuitem")
const count = await items.count()
// Export availability depends on runtime config; assert on the stable items.
expect([3, 4]).toContain(count)
const labels = await items.allTextContents()
expect(labels[0]).toMatch(/Pin|置顶/)
expect(labels[1]).toMatch(/Rename|重命名/)
expect(labels[count - 1]).toMatch(/Delete|删除/)
if (count === 4) {
expect(labels[2]).toMatch(/Export|导出/)
}

// Each item carries a leading icon-svg slot.
for (let i = 0; i < count; i++) {
await expect(items.nth(i).locator('[data-slot="icon-svg"]')).toBeVisible()
}
})
})

test("sort trigger opens a dropdown with two options and reflects the active mode", async ({ page, sdk, gotoSession }) => {
const stamp = Date.now()
await withSession(sdk, `slice 09 sort ${stamp}`, async (session) => {
await gotoSession(session.id)
await openSidebar(page)

const sidebar = page.locator(pawworkSidebarSelector).first()
const trigger = sidebar.locator('[data-action="pawwork-sort-trigger"]').first()
await expect(trigger).toBeVisible()
await trigger.click()

const options = page.locator('[data-action="pawwork-sort-option"]')
await expect(options).toHaveCount(2)
await expect(page.locator('[data-action="pawwork-sort-option"][data-value="time"]')).toBeVisible()
await expect(page.locator('[data-action="pawwork-sort-option"][data-value="project"]')).toBeVisible()

await page.locator('[data-action="pawwork-sort-option"][data-value="project"]').click()
await expect(trigger).toHaveAttribute("data-mode", "project")
})
})

test("L37 three-segment shape: side-top, side-scroll, side-foot stacked", async ({
page,
sdk,
gotoSession,
}) => {
// The side-traffic placeholder is reserved for slice 17 (when traffic-lights
// and collapse control move into the sidebar). Until then, the OS chrome
// already supplies that space, so we ship without the placeholder.
const stamp = Date.now()
await withSession(sdk, `slice 09 shape ${stamp}`, async (session) => {
await gotoSession(session.id)
await openSidebar(page)

const sidebar = page.locator(pawworkSidebarSelector).first()
const top = sidebar.locator('[data-component="pawwork-side-top"]').first()
const scroll = sidebar.locator('[data-component="pawwork-side-scroll"]').first()
const foot = sidebar.locator('[data-component="pawwork-side-foot"]').first()

await expect(top).toBeVisible()
await expect(scroll).toBeVisible()
await expect(foot).toBeVisible()

const topBox = await top.boundingBox()
const scrollBox = await scroll.boundingBox()
const footBox = await foot.boundingBox()
expect(topBox).not.toBeNull()
expect(scrollBox).not.toBeNull()
expect(footBox).not.toBeNull()
expect(topBox!.y).toBeLessThan(scrollBox!.y)
expect(scrollBox!.y).toBeLessThan(footBox!.y)
})
})
1 change: 0 additions & 1 deletion packages/app/src/components/dialog-select-file.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,6 @@ export function DialogSelectFile(props: { mode?: DialogSelectFileMode; onOpenFil
? language.t("session.header.searchFiles")
: language.t("palette.search.placeholder"),
autofocus: true,
hideIcon: true,
}}
emptyMessage={language.t("palette.empty")}
loadingMessage={language.t("common.loading")}
Expand Down
6 changes: 3 additions & 3 deletions packages/app/src/components/prompt-input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1186,7 +1186,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
size: "normal",
style: triggerStyle(),
class:
"min-w-0 px-1.5 justify-start! text-13-regular! text-fg-base group",
"min-w-0 px-1.5 justify-start text-13-regular text-fg-base font-normal group",
"data-action": "prompt-model",
"data-picker-trigger": "",
disabled: !actionReady(),
Expand All @@ -1201,7 +1201,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
/>
</Show>
<span
class="truncate text-center max-w-[7rem] transition-[max-width] duration-200 ease-out"
class="truncate text-center max-w-[7rem] transition-[max-width] duration-200 ease-out font-normal"
classList={{ "@max-[28rem]/composer:max-w-0": !!local.model.current()?.provider?.id }}
>
{local.model.current()?.name ?? language.t("dialog.model.select.title")}
Expand Down Expand Up @@ -1251,7 +1251,7 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
triggerProps={{
"data-action": "prompt-model-variant",
class:
"max-w-[160px] @max-[20rem]/composer:max-w-[80px] text-13-regular! text-fg-base",
"max-w-[160px] @max-[20rem]/composer:max-w-[80px] text-13-regular text-fg-base font-normal",
}}
/>
</TooltipKeybind>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export function WorkspaceChip(props: { style?: JSX.CSSProperties | string } = {}
"aria-label": language.t("workspace.chip.ariaLabel"),
"aria-haspopup": "menu",
class:
"px-1.5 inline-flex items-center gap-1.5 text-13-regular text-fg-base",
"px-1.5 inline-flex items-center gap-1.5 text-13-regular text-fg-base font-normal",
style: props.style,
} as any
}
Expand Down
10 changes: 9 additions & 1 deletion packages/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,11 @@ export const dict = {
"dialog.project.edit.icon.readFailed.title": "Failed to read image",
"dialog.project.edit.icon.readFailed.description": "The selected file could not be loaded. Please try another image.",

"dialog.releaseNotes.action.getStarted": "Get started",
"dialog.releaseNotes.action.next": "Next",
"dialog.releaseNotes.action.hideFuture": "Don't show these in the future",
"dialog.releaseNotes.media.alt": "Release preview",

"context.breakdown.title": "Context Breakdown",
"context.breakdown.note": 'Approximate breakdown of input tokens. "Other" includes tool definitions and overhead.',
"context.breakdown.system": "System",
Expand Down Expand Up @@ -804,6 +809,9 @@ export const dict = {
"sidebar.pawwork.unpinSession": "Unpin session",
"sidebar.pawwork.sort.byProject": "Group by project",
"sidebar.pawwork.sort.byTime": "Sort by time",
"sidebar.pawwork.sort.label": "Sort",
"sidebar.pawwork.sort.optionByTime": "By time",
"sidebar.pawwork.sort.optionByProject": "By project",
"sidebar.pawwork.searchHistory": "Use Search for older sessions",

"debugBar.ariaLabel": "Development performance diagnostics",
Expand All @@ -830,7 +838,7 @@ export const dict = {
"debugBar.mem.tipUnavailable": "Used JS heap vs heap limit. Chromium only.",
"debugBar.mem.tip": "Used JS heap vs heap limit. {{used}} of {{limit}}.",

"app.name.desktop": "PawWork Desktop",
"app.name.desktop": "PawWork",

"settings.section.desktop": "Desktop",
"settings.section.server": "Server",
Expand Down
7 changes: 7 additions & 0 deletions packages/app/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,9 @@ export const dict = {
"sidebar.pawwork.unpinSession": "取消置顶",
"sidebar.pawwork.sort.byProject": "按项目分组",
"sidebar.pawwork.sort.byTime": "按时间排序",
"sidebar.pawwork.sort.label": "排序",
"sidebar.pawwork.sort.optionByTime": "按时间",
"sidebar.pawwork.sort.optionByProject": "按项目",
"sidebar.pawwork.searchHistory": "搜索历史会话",

"app.name.desktop": "爪印",
Expand Down Expand Up @@ -1016,6 +1019,10 @@ export const dict = {
"workspace.reset.archived.many": "将归档 {{count}} 个会话。",
"workspace.reset.note": "这将把工作区重置为与默认分支一致。",
"common.open": "打开",
"dialog.releaseNotes.action.getStarted": "开始",
"dialog.releaseNotes.action.next": "下一步",
"dialog.releaseNotes.action.hideFuture": "不再显示",
"dialog.releaseNotes.media.alt": "发布预览",
"toast.project.reloadFailed.title": "无法重新加载 {{project}}",
"error.server.invalidConfiguration": "配置无效",
"common.moreCountSuffix": " (还有 {{count}} 个)",
Expand Down
32 changes: 21 additions & 11 deletions packages/app/src/pages/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,14 @@ export default function Layout(props: ParentProps) {
const notification = useNotification()
const permission = usePermission()
const navigate = useNavigate()
setNavigate(navigate)
// Wrap navigate so non-shell entry points (notification clicks, deep links
// dispatched through @/utils/notification-click) also close the settings
// overlay before routing. Shell-driven navigation (openNewSession /
// openSession) closes settings explicitly through closeSettingsSurface.
setNavigate((href) => {
closeSettings()
navigate(href)
})
const providers = useProviders()
const dialog = useDialog()
const command = useCommand()
Expand Down Expand Up @@ -1088,6 +1095,14 @@ export default function Layout(props: ParentProps) {
setStore("pawworkSortMode", mode)
}

function toggleProjectCollapsed(label: string) {
const current = store.pawworkProjectCollapsed
const next: Record<string, boolean> = { ...current }
if (next[label]) delete next[label]
else next[label] = true
setStore("pawworkProjectCollapsed", reconcile(next))
}

// Export hits the embedded sidecar via main-process IPC. When the user has
// switched the active server to a remote target, the sidecar holds different
// data than the UI; hide the action rather than ship a misleading export.
Expand Down Expand Up @@ -1446,16 +1461,6 @@ export default function Layout(props: ParentProps) {
setSettingsOpen(false)
}

let lastSettingsPath: string | undefined
createEffect(() => {
const current = location.pathname
if (settingsOpen()) {
if (lastSettingsPath === undefined) lastSettingsPath = current
else if (lastSettingsPath !== current) closeSettings()
} else {
lastSettingsPath = undefined
}
})

function projectRoot(directory: string) {
const key = workspaceKey(directory)
Expand Down Expand Up @@ -1524,6 +1529,7 @@ export default function Layout(props: ParentProps) {
currentProjectRoot: () => currentProject()?.worktree ?? projectRoot(currentDir()),
chooseProject,
openSettingsSurface,
closeSettingsSurface: closeSettings,
})

function openProject(directory: string, shouldNavigate = true) {
Expand Down Expand Up @@ -2151,6 +2157,8 @@ export default function Layout(props: ParentProps) {
activeSessionID={() => params.id}
pinnedIDs={() => store.pawworkPinnedSessions}
sortMode={() => store.pawworkSortMode}
collapsedProjects={() => store.pawworkProjectCollapsed}
onToggleProjectCollapsed={toggleProjectCollapsed}
setScrollContainerRef={workspaceSidebarCtx.setScrollContainerRef}
prefetchSession={prefetchSession}
onOpenSession={navigateToSession}
Expand All @@ -2168,6 +2176,8 @@ export default function Layout(props: ParentProps) {
onOpenSettings={openSettings}
settingsLabel={() => language.t("sidebar.settings")}
settingsKeybind={() => command.keybind("settings.open")}
newSessionKeybind={() => command.keybind("session.new")}
searchKeybind={() => command.keybind("command.palette")}
/>
)
const sidebarContent = () =>
Expand Down
Loading
Loading