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
134 changes: 134 additions & 0 deletions docs/superpowers/plans/2026-08-05-issue-936-task-dialog-overflow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# Issue #936 Task Dialog Overflow Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Keep the stop-task and delete-task confirmation actions visible when a task name is longer than the viewport.

**Architecture:** Apply viewport height and two-row grid constraints only to the two task action dialogs in `NavProject`. Make each dialog header a keyboard-accessible scrollable region, keep its footer outside that region, and add a source-contract regression test matching the frontend test conventions.

**Tech Stack:** React 19, TypeScript, Tailwind CSS 4, Radix AlertDialog, Node test runner, tsx, pnpm, Vite

## Global Constraints

- Modify only the project navigation task dialogs and their focused regression test.
- Keep the shared `AlertDialog`, APIs, state handling, i18n resources, and region logic unchanged.
- Cover both stop-task and delete-task dialogs.
- Preserve complete task names while supporting continuous strings and normal word wrapping.
- Run the focused test, frontend lint, online build, and overseas SaaS preview.
- Commit implementation only after the user approves the preview.

---

### Task 1: Constrain task action dialogs to the viewport

**Files:**
- Create: `frontend/test/console-nav-project-dialog-layout.test.ts`
- Modify: `frontend/src/components/console/nav/nav-project.tsx:409-454`

**Interfaces:**
- Consumes: the existing `AlertDialogContent`, `AlertDialogHeader`, `AlertDialogDescription`, and `AlertDialogFooter` components
- Produces: two dialogs with scrollable descriptions and always-visible action footers

- [x] **Step 1: Add the failing layout contract test**

Create `frontend/test/console-nav-project-dialog-layout.test.ts`:

```typescript
import assert from "node:assert/strict"
import { readFileSync } from "node:fs"
import test from "node:test"

const source = readFileSync(
new URL("../src/components/console/nav/nav-project.tsx", import.meta.url),
"utf8",
)

function countOccurrences(value: string) {
return source.split(value).length - 1
}

test("任务操作弹窗限制在视口内并保持操作区可见", () => {
assert.equal(
countOccurrences(
'className="max-h-[calc(100dvh-2rem)] grid-rows-[minmax(0,1fr)_auto] overflow-hidden"',
),
2,
)
assert.equal(
countOccurrences('className="min-h-0 overflow-y-auto overscroll-contain"'),
2,
)
assert.equal(
countOccurrences('className="break-words [overflow-wrap:anywhere]"'),
2,
)
})
```

- [x] **Step 2: Run the test and confirm the regression**

Run:

```bash
tsx --test frontend/test/console-nav-project-dialog-layout.test.ts
```

Expected: FAIL because all three occurrence counts are `0` instead of `2`.

- [x] **Step 3: Apply the layout constraints to both dialogs**

On both the delete-task and stop-task dialogs, replace the three existing opening tags with these exact tags. Keep every child node, footer, handler, disabled state, and localized string in its current order:

```tsx
<AlertDialogContent className="max-h-[calc(100dvh-2rem)] grid-rows-[minmax(0,1fr)_auto] overflow-hidden">
<AlertDialogHeader
role="region"
tabIndex={0}
aria-label={t("navProject.deleteTask.title")}
className="min-h-0 overflow-y-auto overscroll-contain outline-hidden ring-ring focus-visible:ring-2 focus-visible:ring-inset"
>
<AlertDialogDescription className="break-words [overflow-wrap:anywhere]">
```

- [x] **Step 4: Run focused frontend tests**

Run:

```bash
tsx --test frontend/test/console-nav-project-dialog-layout.test.ts frontend/test/console-nav-project-i18n.test.ts
```

Expected: 3 tests pass.

- [x] **Step 5: Run frontend static validation**

Run:

```bash
pnpm --dir frontend lint
pnpm --dir frontend run build:online
git diff --check
```

Expected: lint and online build succeed; `git diff --check` has no output. Existing Vite chunk-size warnings may remain informational.

- [x] **Step 6: Start the overseas SaaS preview**

Run the Vite development server from `frontend` with:

```bash
TARGET=https://monkeycode-ai.net pnpm dev:online
```

Use an available preview port. At 100% browser zoom, verify both dialogs with a roughly 600-word task name at desktop 1366×768 and a mobile viewport. The task name region scrolls, the footer remains visible, and cancel/confirm actions remain clickable.

Preview evidence: the overseas SaaS preview was served successfully on 2026-08-05, and the user confirmed the rendered result looked correct before authorizing the commit and pull request.

- [x] **Step 7: Present evidence and wait for commit approval**

Show the changed files, focused test results, lint result, online build result, preview URL, and manual verification checklist. After user approval, stage only:

```bash
git add docs/superpowers/specs/2026-08-05-issue-936-task-dialog-overflow-design.md docs/superpowers/plans/2026-08-05-issue-936-task-dialog-overflow.md frontend/src/components/console/nav/nav-project.tsx frontend/test/console-nav-project-dialog-layout.test.ts
git commit -m "修复:限制任务操作弹窗高度"
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Issue #936 任务操作弹窗溢出修复设计

## 状态

- 日期:2026-08-05
- Issue:https://github.com/chaitin/MonkeyCode/issues/936
- 分支:`260805-fix-936-stop-dialog-overflow`
- 状态:设计已确认并完成实施

## 问题

项目导航中的终止任务和删除任务确认弹窗会把完整任务名称插入描述。长任务名称产生大量换行时,弹窗高度超过当前视口,底部取消和确认按钮移出屏幕。现有弹窗缺少视口高度约束、正文滚动区域和任意长文本断行规则。

该入口使用国内版与海外版共用的 `NavProject` 组件。英文长任务名称更容易触发,两个区域均受相同布局影响。

## 目标

1. 终止任务与删除任务弹窗始终保持在可见视口内。
2. 长任务名称在正文区域内换行和滚动。
3. 底部取消与确认按钮始终保留在弹窗内的固定操作区。
4. 短任务名称和现有桌面、移动端布局保持当前视觉行为。
5. 修复范围限定在项目导航的两个任务操作弹窗。

## 方案选择

### 采用方案:局部双弹窗约束

在 `nav-project.tsx` 中为终止和删除任务弹窗应用相同布局:

- `AlertDialogContent` 使用基于 `100dvh` 的最大高度。
- Content 的网格行分为可收缩正文和固定 Footer。
- Header 作为可滚动正文区域,允许垂直滚动。
- Description 对连续长文本启用任意位置断行。
- Footer 保持在滚动区域之外。

此方案覆盖两个使用完整任务名称的相邻入口,并保持公共 `AlertDialog` 的其他消费者不变。

### 备选方案

1. 仅修终止任务弹窗:改动最少,删除任务保留同类溢出风险。
2. 修改公共 `AlertDialog`:所有确认弹窗获得统一高度行为,同时扩大回归范围并可能改变现有复杂弹窗布局。

## 组件设计

### AlertDialogContent

两个弹窗增加等价的局部类名:

- 最大高度:`max-h-[calc(100dvh-2rem)]`
- 网格行:`grid-rows-[minmax(0,1fr)_auto]`
- 内容裁剪:`overflow-hidden`

视口上下各保留 `1rem` 安全间距。第一行允许缩小,第二行根据 Footer 内容保持自然高度。

### AlertDialogHeader

Header 增加:

- `min-h-0`
- `overflow-y-auto`
- `overscroll-contain`
- `role="region"`、`tabIndex={0}` 和本地化 `aria-label`
- 可见的 `focus-visible` 焦点环

长任务名称只在 Header 内滚动,滚动到边界时保持弹窗上下文稳定。键盘用户可以聚焦该区域并使用方向键、Page Up 或 Page Down 浏览完整内容。

### AlertDialogDescription

Description 增加:

- `break-words`
- `[overflow-wrap:anywhere]`

普通英文句子按空格换行,连续 URL、仓库名或无空格字符串也能在弹窗宽度内断行。

## 数据与交互

任务数据、停止接口、删除接口和状态管理保持原状。布局变化只作用于确认弹窗:

1. 用户选择终止或删除任务。
2. 完整任务名称进入本地化描述。
3. 短内容保持自然高度。
4. 长内容达到最大高度后,Header 内部滚动。
5. Footer 始终显示取消与确认按钮。

关闭弹窗、请求进行中禁用按钮、成功刷新任务列表和错误提示继续使用现有逻辑。

## 测试

新增一个聚焦布局契约的 TypeScript 测试,读取 `nav-project.tsx` 并验证:

1. 终止和删除两个 `AlertDialogContent` 都具有视口最大高度、双行网格和内容裁剪。
2. 两个 Header 都具有可收缩、垂直滚动、overscroll 约束和键盘焦点入口。
3. 两个 Description 都具有单词断行和任意位置断行。
4. Footer 位于 Header 之后,保持独立操作区。

验证命令:

```bash
tsx --test frontend/test/console-nav-project-dialog-layout.test.ts
pnpm --dir frontend lint
pnpm --dir frontend run build:online
```

使用海外 SaaS 预览完成手工验证:

1. 浏览器保持 100% 缩放。
2. 使用约 600 词的任务名称。
3. 在 1366×768 和移动端视口打开终止、删除弹窗。
4. 验证任务名称区域可滚动,两个操作按钮始终可见且可点击。

## 验收标准

1. 终止任务与删除任务弹窗在长任务名称下保持在视口内。
2. Footer 无需缩放浏览器即可访问。
3. 连续长字符串不会横向撑开弹窗。
4. 两个操作流程的 API 与状态行为保持原状。
5. 定向测试、Lint 和 `build:online` 通过。
6. 海外 SaaS 预览通过桌面和移动视口人工验证。

## 范围边界

- 不修改公共 `AlertDialog`。
- 不截断或改写任务名称。
- 不修改后端、API、国际化文案和区域判断。
- 不处理 Issue #936 之外的侧边栏交互。
22 changes: 16 additions & 6 deletions frontend/src/components/console/nav/nav-project.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -407,10 +407,15 @@ export default function NavProject() {
</SidebarMenu>
)}
<AlertDialog open={!!taskToDelete} onOpenChange={(open) => !open && setTaskToDelete(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogContent className="max-h-[calc(100dvh-2rem)] grid-rows-[minmax(0,1fr)_auto] overflow-hidden">
<AlertDialogHeader
role="region"
tabIndex={0}
aria-label={t("navProject.deleteTask.title")}
className="min-h-0 overflow-y-auto overscroll-contain outline-hidden ring-ring focus-visible:ring-2 focus-visible:ring-inset"
>
<AlertDialogTitle>{t("navProject.deleteTask.title")}</AlertDialogTitle>
<AlertDialogDescription>
<AlertDialogDescription className="break-words [overflow-wrap:anywhere]">
{t("navProject.deleteTask.description", { task: getTaskDisplayName(taskToDelete) })}
</AlertDialogDescription>
</AlertDialogHeader>
Expand All @@ -430,10 +435,15 @@ export default function NavProject() {
</AlertDialogContent>
</AlertDialog>
<AlertDialog open={!!taskToStop} onOpenChange={(open) => !open && setTaskToStop(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogContent className="max-h-[calc(100dvh-2rem)] grid-rows-[minmax(0,1fr)_auto] overflow-hidden">
<AlertDialogHeader
role="region"
tabIndex={0}
aria-label={t("navProject.stopTask.title")}
className="min-h-0 overflow-y-auto overscroll-contain outline-hidden ring-ring focus-visible:ring-2 focus-visible:ring-inset"
>
<AlertDialogTitle>{t("navProject.stopTask.title")}</AlertDialogTitle>
<AlertDialogDescription>
<AlertDialogDescription className="break-words [overflow-wrap:anywhere]">
{t("navProject.stopTask.description", { task: getTaskDisplayName(taskToStop) })}
</AlertDialogDescription>
</AlertDialogHeader>
Expand Down
50 changes: 50 additions & 0 deletions frontend/test/console-nav-project-dialog-layout.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import assert from "node:assert/strict"
import { readFileSync } from "node:fs"
import test from "node:test"

const source = readFileSync(
new URL("../src/components/console/nav/nav-project.tsx", import.meta.url),
"utf8",
)

function getDialogSource(stateName: string) {
const start = source.indexOf(`<AlertDialog open={!!${stateName}}`)
assert.notEqual(start, -1, `找不到 ${stateName} 弹窗`)

const end = source.indexOf("</AlertDialog>", start)
assert.notEqual(end, -1, `找不到 ${stateName} 弹窗结束标签`)

return source.slice(start, end)
}

function assertDialogLayout(stateName: string, titleKey: string) {
const dialogSource = getDialogSource(stateName)

assert.match(
dialogSource,
/<AlertDialogContent className="max-h-\[calc\(100dvh-2rem\)\] grid-rows-\[minmax\(0,1fr\)_auto\] overflow-hidden">/,
)
assert.ok(
dialogSource.includes(
`<AlertDialogHeader
role="region"
tabIndex={0}
aria-label={t("${titleKey}")}
className="min-h-0 overflow-y-auto overscroll-contain outline-hidden ring-ring focus-visible:ring-2 focus-visible:ring-inset"
>`,
),
)
assert.match(
dialogSource,
/<AlertDialogDescription className="break-words \[overflow-wrap:anywhere\]">/,
)

const headerEnd = dialogSource.indexOf("</AlertDialogHeader>")
const footerStart = dialogSource.indexOf("<AlertDialogFooter>")
assert.ok(headerEnd > -1 && footerStart > headerEnd, "操作区必须位于可滚动 Header 之外")
}

test("任务操作弹窗限制在视口内并保持操作区可见", () => {
assertDialogLayout("taskToDelete", "navProject.deleteTask.title")
assertDialogLayout("taskToStop", "navProject.stopTask.title")
})
Loading