Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions packages/app/e2e/selectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export const sidebarNavMobileSelector = '[data-component="sidebar-nav-mobile"]'
export const popoverBodySelector = '[data-slot="popover-body"]'

export const dropdownMenuContentSelector = '[data-component="dropdown-menu-content"]'
export const contextMenuContentSelector = '[data-component="context-menu-content"]'

export const inlineInputSelector = '[data-component="input"][data-variant="inline"]'

Expand Down
28 changes: 21 additions & 7 deletions packages/app/e2e/sidebar/sidebar-project-actions.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { test, expect } from "../fixtures"
import { openSidebar, withSession, clickMenuItem } from "../actions"
import { pawworkSidebarSelector, dropdownMenuContentSelector } from "../selectors"
import { pawworkSidebarSelector, contextMenuContentSelector, dropdownMenuContentSelector } from "../selectors"
import type { TestInfo } from "@playwright/test"

async function capture(page: any, testInfo: TestInfo, name: string) {
Expand Down Expand Up @@ -37,13 +37,27 @@ test("project group can be renamed from sidebar", async ({ page, sdk, gotoSessio
// Screenshot: project menu opened
await capture(page, testInfo, `02-project-menu-${stamp}`)

// Click rename
const menu = page.locator(dropdownMenuContentSelector).first()
await expect(menu).toBeVisible()
await expect(menu).toContainText("Rename project")
await expect(menu).not.toContainText("project.rename")
await page.keyboard.press("Escape")

await header.click({ button: "right" })
const contextMenu = page.locator(contextMenuContentSelector).first()
await expect(contextMenu).toBeVisible()
await expect(contextMenu).toContainText("Rename project")
await expect(contextMenu).not.toContainText("project.rename")
await page.keyboard.press("Escape")

await menuTrigger.click()
await expect(menu).toBeVisible()
await clickMenuItem(menu, /Rename project/)

// Dialog should appear
const dialog = page.locator('[data-component="dialog"]').filter({ hasText: /Rename project/ }).first()
await expect(dialog).toBeVisible()
await expect(dialog).not.toContainText("project.rename")

// Screenshot: rename dialog
await capture(page, testInfo, `03-rename-dialog-${stamp}`)
Expand Down Expand Up @@ -96,10 +110,10 @@ test("project group can be removed from sidebar", async ({ page, sdk, gotoSessio

// Click remove
const menu = page.locator(dropdownMenuContentSelector).first()
await clickMenuItem(menu, /Remove from sidebar/)
await clickMenuItem(menu, /Remove project/)

// Confirm dialog should appear
const dialog = page.locator('[data-component="dialog"]').filter({ hasText: /Remove project from sidebar/ }).first()
const dialog = page.locator('[data-component="dialog"]').filter({ hasText: /Remove project/ }).first()
await expect(dialog).toBeVisible()

// Screenshot: remove confirm dialog
Expand Down Expand Up @@ -145,9 +159,9 @@ test("hidden project restores on direct navigation", async ({ page, sdk, gotoSes
await menuTrigger.click()

const menu = page.locator(dropdownMenuContentSelector).first()
await clickMenuItem(menu, /Remove from sidebar/)
await clickMenuItem(menu, /Remove project/)

const dialog = page.locator('[data-component="dialog"]').filter({ hasText: /Remove project from sidebar/ }).first()
const dialog = page.locator('[data-component="dialog"]').filter({ hasText: /Remove project/ }).first()
await expect(dialog).toBeVisible()
await dialog.locator('button').filter({ hasText: /Remove/ }).first().click()

Expand All @@ -171,4 +185,4 @@ test("hidden project restores on direct navigation", async ({ page, sdk, gotoSes
await expect.poll(async () => await groupsAfter.count()).toBeGreaterThan(countAfterRemove)
})
})
})
})
11 changes: 11 additions & 0 deletions packages/app/src/i18n/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,7 @@ export const dict = {
"common.retry": "Retry",
"common.showMore": "Show more",
"common.cancel": "Cancel",
"common.undo": "Undo",
"common.open": "Open",
"common.connect": "Connect",
"common.disconnect": "Disconnect",
Expand Down Expand Up @@ -496,6 +497,8 @@ export const dict = {
"toast.session.unshare.failed.description": "An error occurred while unsharing the session",
"toast.session.rename.failed.title": "Failed to rename session",
"toast.session.rename.failed.description": "Try again, or close the dialog to keep the current name.",
"toast.project.rename.failed.title": "Failed to rename project",
"toast.project.rename.failed.description": "Try again, or close the dialog to keep the current name.",

"toast.session.listFailed.title": "Failed to load sessions for {{project}}",
"toast.project.reloadFailed.title": "Failed to reload {{project}}",
Expand Down Expand Up @@ -767,6 +770,7 @@ export const dict = {
"common.moreOptions": "More options",
"common.learnMore": "Learn more",
"common.rename": "Rename",
"common.remove": "Remove",
"common.reset": "Reset",
"common.delete": "Delete",
"common.close": "Close",
Expand Down Expand Up @@ -1123,6 +1127,13 @@ export const dict = {
"settings.permissions.tool.doom_loop.description": "Detect repeated tool calls with identical input",

"session.rename.title": "Rename chat",
"project.rename": "Rename project",
"project.remove": "Remove project",
"project.remove.title": "Remove project",
"project.remove.confirm": 'Remove "{{name}}"?',
"project.remove.description": "The project will be hidden from the sidebar. You can undo this.",
"project.remove.toast.title": "Project removed",
"project.remove.toast.description": "The project has been hidden from the sidebar.",
"session.delete.failed.title": "Failed to delete session",
"session.delete.title": "Delete session",
"session.delete.confirm": 'Delete session "{{name}}"?',
Expand Down
11 changes: 11 additions & 0 deletions packages/app/src/i18n/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,7 @@ export const dict = {
"common.retry": "重试",
"common.showMore": "显示更多",
"common.cancel": "取消",
"common.undo": "撤销",
"common.connect": "连接",
"common.disconnect": "断开连接",
"common.continue": "提交",
Expand Down Expand Up @@ -486,6 +487,8 @@ export const dict = {
"toast.session.unshare.failed.description": "取消分享会话时发生错误",
"toast.session.rename.failed.title": "重命名失败",
"toast.session.rename.failed.description": "请重试,或关闭对话框保持当前名称。",
"toast.project.rename.failed.title": "重命名失败",
"toast.project.rename.failed.description": "请重试,或关闭对话框保持当前名称。",
"toast.session.listFailed.title": "无法加载 {{project}} 的会话",
"toast.update.title": "有可用更新",
"toast.update.description": "爪印有新版本 ({{version}}) 可安装。",
Expand Down Expand Up @@ -699,6 +702,7 @@ export const dict = {
"common.moreOptions": "更多选项",
"common.learnMore": "了解更多",
"common.rename": "重命名",
"common.remove": "移除",
"common.reset": "重置",
"common.delete": "删除",
"common.close": "关闭",
Expand Down Expand Up @@ -1004,6 +1008,13 @@ export const dict = {
"settings.permissions.tool.doom_loop.description": "检测具有相同输入的重复工具调用",

"session.rename.title": "重命名对话",
"project.rename": "重命名项目",
"project.remove": "移除项目",
"project.remove.title": "移除项目",
"project.remove.confirm": "移除「{{name}}」?",
"project.remove.description": "该项目将从侧边栏隐藏,可以撤销。",
"project.remove.toast.title": "项目已移除",
"project.remove.toast.description": "项目已从侧边栏隐藏。",
"session.delete.failed.title": "删除会话失败",
"session.delete.title": "删除会话",
"session.delete.confirm": '删除会话 "{{name}}"?',
Expand Down
155 changes: 89 additions & 66 deletions packages/app/src/pages/layout/pawwork-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,87 @@ export type PawworkSidebarSession = {
created: number
}

function ProjectGroupHeader(props: {
projectKey: string
label: string
collapsed: boolean
onToggle: () => void
onRename: () => void
onRemove: () => void
}) {
const language = useLanguage()
const projectMenuLabels = () => ({
rename: language.t("project.rename"),
remove: language.t("project.remove"),
})

return (
<ContextMenu>
<ContextMenu.Trigger as="div">
<div
data-component="pawwork-group-header"
data-collapsed={props.collapsed ? "true" : undefined}
title={props.projectKey}
class="group/group-header h-[30px] w-full flex items-center rounded-sm text-13-regular text-fg-weak transition-colors hover:bg-row-hover-overlay focus-within:bg-row-hover-overlay"
>
<button
type="button"
data-action="pawwork-group-toggle"
data-collapsed={props.collapsed ? "true" : undefined}
aria-expanded={!props.collapsed}
onClick={props.onToggle}
class="min-w-0 h-full flex-1 flex items-center gap-2 px-2.5 text-left focus:outline-none"
>
<Icon
name={props.collapsed ? "folder" : "folder-open"}
class="shrink-0 text-icon-weak"
/>
<span class="min-w-0 flex-1 truncate">{props.label}</span>
</button>
<div class="pointer-events-none relative shrink-0 flex items-center justify-end h-[20px] min-w-[30px] pr-1">
<div class="absolute inset-y-0 right-1 flex items-center justify-end opacity-0 pointer-events-none group-hover/group-header:opacity-100 group-hover/group-header:pointer-events-auto group-focus-within/group-header:opacity-100 group-focus-within/group-header:pointer-events-auto group-has-[[data-expanded]]/group-header:opacity-100 group-has-[[data-expanded]]/group-header:pointer-events-auto">
<DropdownMenu>
<DropdownMenu.Trigger
as={IconButton}
icon="dot-grid"
variant="ghost"
class="pointer-events-auto h-[26px] w-[26px]"
data-action="project-row-menu"
aria-label={language.t("common.moreOptions")}
/>
<DropdownMenu.Portal>
<DropdownMenu.Content>
<DropdownMenu.Item onSelect={props.onRename}>
<Icon name="edit" class="text-icon-weak" />
<DropdownMenu.ItemLabel>{projectMenuLabels().rename}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Item onSelect={props.onRemove}>
<Icon name="archive" class="text-icon-weak" />
<DropdownMenu.ItemLabel>{projectMenuLabels().remove}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
</div>
</div>
</div>
</ContextMenu.Trigger>
<ContextMenu.Portal>
<ContextMenu.Content>
<ContextMenu.Item onSelect={props.onRename}>
<Icon name="edit" class="text-icon-weak" />
<ContextMenu.ItemLabel>{projectMenuLabels().rename}</ContextMenu.ItemLabel>
</ContextMenu.Item>
<ContextMenu.Item onSelect={props.onRemove}>
<Icon name="archive" class="text-icon-weak" />
<ContextMenu.ItemLabel>{projectMenuLabels().remove}</ContextMenu.ItemLabel>
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Portal>
</ContextMenu>
)
}

export const PawworkSidebar = (props: {
scope?: "main" | "peek"
sessions: Accessor<PawworkSidebarSession[]>
Expand Down Expand Up @@ -403,77 +484,19 @@ export const PawworkSidebar = (props: {
<For each={groupedRows()}>
{(group, index) => {
const collapsed = createMemo(() => !!props.collapsedProjects()[group.key])
const projectMenuLabels = () => ({
rename: language.t("project.rename"),
remove: language.t("project.remove"),
})
const handleRename = () => openRenameProjectDialog(group.key, group.label)
const handleRemove = () => openRemoveProjectDialog(group.key, group.label)

return (
<section class={`${index() > 0 ? "mt-0.5 " : ""}flex flex-col gap-0.5`}>
<ContextMenu>
<ContextMenu.Trigger as="div">
<button
type="button"
data-component="pawwork-group-header"
data-action="pawwork-group-toggle"
data-collapsed={collapsed() ? "true" : undefined}
aria-expanded={!collapsed()}
title={group.key}
onClick={() => props.onToggleProjectCollapsed(group.key)}
class="group/group-header h-[30px] w-full flex items-center gap-2 rounded-sm px-2.5 text-13-regular text-fg-weak transition-colors hover:bg-row-hover-overlay focus:outline-none focus-visible:bg-row-hover-overlay"
>
<Icon
name={collapsed() ? "folder" : "folder-open"}
class="shrink-0 text-icon-weak"
/>
<span class="min-w-0 flex-1 truncate text-left">{group.label}</span>
<div class="pointer-events-none relative shrink-0 flex items-center justify-end h-[20px] min-w-[20px]">
<div class="absolute inset-y-0 right-0 flex items-center justify-end opacity-0 pointer-events-none group-hover/group-header:opacity-100 group-hover/group-header:pointer-events-auto group-focus-visible/group-header:opacity-100 group-focus-visible/group-header:pointer-events-auto group-has-[[data-expanded]]/group-header:opacity-100 group-has-[[data-expanded]]/group-header:pointer-events-auto">
<DropdownMenu>
<DropdownMenu.Trigger
as={IconButton}
icon="dot-grid"
variant="ghost"
class="pointer-events-auto h-[26px] w-[26px]"
data-action="project-row-menu"
aria-label={language.t("common.moreOptions")}
onClick={(event: MouseEvent) => {
event.preventDefault()
event.stopPropagation()
}}
/>
<DropdownMenu.Portal>
<DropdownMenu.Content>
<DropdownMenu.Item onSelect={handleRename}>
<Icon name="edit" class="text-icon-weak" />
<DropdownMenu.ItemLabel>{projectMenuLabels().rename}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
<DropdownMenu.Item onSelect={handleRemove}>
<Icon name="archive" class="text-icon-weak" />
<DropdownMenu.ItemLabel>{projectMenuLabels().remove}</DropdownMenu.ItemLabel>
</DropdownMenu.Item>
</DropdownMenu.Content>
</DropdownMenu.Portal>
</DropdownMenu>
</div>
</div>
</button>
</ContextMenu.Trigger>
<ContextMenu.Portal>
<ContextMenu.Content>
<ContextMenu.Item onSelect={handleRename}>
<Icon name="edit" class="text-icon-weak" />
<ContextMenu.ItemLabel>{projectMenuLabels().rename}</ContextMenu.ItemLabel>
</ContextMenu.Item>
<ContextMenu.Item onSelect={handleRemove}>
<Icon name="archive" class="text-icon-weak" />
<ContextMenu.ItemLabel>{projectMenuLabels().remove}</ContextMenu.ItemLabel>
</ContextMenu.Item>
</ContextMenu.Content>
</ContextMenu.Portal>
</ContextMenu>
<ProjectGroupHeader
projectKey={group.key}
label={group.label}
collapsed={collapsed()}
onToggle={() => props.onToggleProjectCollapsed(group.key)}
onRename={handleRename}
onRemove={handleRemove}
/>
{/* grid-template-rows trick: 0fr → 1fr animates height without
* touching layout-thrashing properties. Items stay mounted so
* focus / scroll position survive the toggle; inert on the
Expand Down
Loading
Loading