Skip to content

Commit c20d361

Browse files
committed
feat(proposal): add detail slideover spec deltas
1 parent 7bc1a57 commit c20d361

49 files changed

Lines changed: 2471 additions & 410 deletions

File tree

Some content is hidden

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

electron.vite.config.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,11 @@ export default defineConfig(({ command }) => ({
121121
},
122122
},
123123
},
124+
slideover: {
125+
slots: {
126+
overlay: "backdrop-blur-sm bg-slate-900/40",
127+
},
128+
},
124129
},
125130
}),
126131
],

guidelines/IPC.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ keywords: [ipc, electron, preload, channels, contracts]
7979
## Proposal Channels
8080

8181
- `proposal:list``proposal:readFile``proposal:apply``proposal:stageStream``proposal:archive``proposal:loadRun``proposal:loadArchive` 等请求-响应 channel 的入参 schema 位于 `src/shared/schemas/ipc/proposal.ts`,handler 位于 `src/main/ipc/proposal.ts`,bridge 与 renderer 薄封装分别位于 `src/preload/api/proposal.ts``src/renderer/src/api/proposal.ts`
82+
- `proposal:getSpecDeltas`:入参 `{ projectId, changeId }`,两个字段均为非空字符串;主进程解析 projectId 后使用 change 定位逻辑读取 `specs/*/spec.md`,返回 `IpcResponse<ProposalSpecDeltaOverview>`。该 channel 只返回 proposal capability delta DTO,不放宽 `proposal:readFile` 的顶层文件读取边界;共享类型位于 `src/shared/types/proposal.ts`,schema 位于 `src/shared/schemas/ipc/proposal.ts`,handler 位于 `src/main/ipc/proposal.ts`,bridge 与 renderer 薄封装分别位于 `src/preload/api/proposal.ts``src/renderer/src/api/proposal.ts`
8283
- `proposal:statusChanged` 是主进程主动向渲染进程推送的广播型 channel:当 `ProposalStatusService` 监听到 `.openspec.yaml``status` 变化、proposal 被归档或删除时,通过 `webContents.send(ProposalChannels.statusChanged, payload)` 发送 `ProposalStatusChangedPayload`。渲染进程通过 `proposalApi.onStatusChanged` 订阅并在 `useSessionStore` 中更新 `sessionProposals`。新增广播 channel 时必须同步更新 `src/shared/types/channels.ts``src/preload/index.d.ts``src/renderer/src/api/`
8384

8485
## Overview Channels

guidelines/RendererProcess.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ keywords: [renderer, vue, pinia, routing, ui]
5353
- MUST: 让 `#body` 复用 `UModal` 默认内边距,不要再用一层 `p-4`/`p-5`/`p-6` 包裹整个 body;只在内部内容块上定义必要的 `gap-*``space-y-*` 或局部 padding。
5454
- MUST: 简单二选一确认弹窗统一通过 `src/renderer/src/composables/useConfirmDialog.ts` 发起。这里的“简单确认”指只有标题、描述、取消/确认按钮,没有额外表单、长说明、代码块、滚动列表或多状态切换。
5555
- MUST: 复杂弹窗继续直接使用 `UModal`,包括需要表单输入、富文本/代码块说明、滚动列表、多状态切换,或确认前必须展示额外上下文的场景;不要为了复用 `useConfirmDialog` 而硬塞复杂内容。
56+
- MUST: 业务级 programmatic overlay(例如 proposal 详情 Slideover)应提供领域 composable 统一打开入口。入口组件只调用 composable,不直接创建 overlay;composable 显式 import 业务组件并使用 `useOverlay().create(Component, { destroyOnClose: true })`,调用方只 await 关闭完成,不依赖 ESC/遮罩关闭返回值表达业务语义。
5657
- MUST: `src/renderer/src/components/shared/ConfirmDialog.vue` 自行通过 `UModal #content` 渲染 icon、标题、描述和操作区,不依赖 `UModal` 默认 header/footer;这是为了避免默认分隔线和局部重复布局,并保持全局确认弹窗视觉统一。
5758
- MUST: `useConfirmDialog()` 打开的确认弹窗必须通过显式按钮关闭并 resolve `Promise<boolean>`;调用方只把它当成确认结果使用,不要依赖 overlay 实例内部状态。
5859
- SHOULD: 将模板中的复杂逻辑下沉到 `stores/``composables/``utils/`,避免在 Vue template 内内联复杂表达式。
@@ -69,6 +70,7 @@ keywords: [renderer, vue, pinia, routing, ui]
6970
- Good: `src/renderer/src/bootstrap/tasks/projects.ts` 调用 store 的 `ensureInitialized()` 预热 persisted project 列表。
7071
- Good: 组件里用 `<a target="_blank" rel="noreferrer">``UButton as="a"` 打开外站,让 `setWindowOpenHandler` 统一走系统浏览器。
7172
- Good: `src/renderer/src/components/task/TaskCard.vue` 的删除确认通过 `useConfirmDialog()` 发起,只在用户确认后继续 emit 删除事件。
73+
- Good: `src/renderer/src/composables/useProposalDetailSlideover.ts` 统一封装 proposal 详情 Slideover,列表页、Overview 和 Chat EventRail 只传入 changeId,不直接操作 overlay 实例或详情路由。
7274
- Good: `src/renderer/src/components/settings/AgentCard.vue` 的卸载确认仍直接使用 `UModal`,因为正文包含卸载命令、附注和更复杂的解释信息。
7375
- Good: `src/renderer/src/components/shared/ConfirmDialog.vue` 使用 `UModal #content` 自定义整块布局,而不是叠加默认 header/footer 分隔线。
7476
- Bad: 在 Vue 组件内直接 `window.api.integration.projectSet(...)`
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
schema: spec-driven
2+
created: 2026-06-26T00:00:00.000Z
3+
status: applying
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
## Context
2+
3+
当前 proposal 详情由 `src/renderer/src/pages/proposal/[id].vue` 承载,依赖 route param 取得 `changeId`,并在页面内组合 `ProposalDetailHeader``ProposalMarkdownContent``ProposalApplySidePanel``useProposalStore``useProposalRunStore``proposalApi.readFile`。入口分散在 proposal 列表、Overview 进行中变更和 Chat EventRail,它们都通过 `router.push('/proposal/<id>')` 跳转到独立详情页。
4+
5+
这次变化有两个约束:
6+
7+
- 用户明确不需要保留旧版 `/proposal/:id` 兼容路径,因此可以删除详情路由和 `pages/proposal` 空壳。
8+
- Proposal 内 `specs/<capability>/spec.md` 是 capability delta,不是主仓库完整 capability spec;UI 应复用 `/specs` 页的信息架构,但不能误导用户把 delta 当作完整规格。
9+
10+
本地 Nuxt UI 4.9 的 `useOverlay` 实现已确认:`open()` 返回带 `result` 的 Promise,`OverlayProvider` 会把组件绑定到 `v-model:open` 并在 `after:leave` 调用 `close(id)`;ESC/遮罩 dismiss 会让 `result` resolve 为 `undefined`。因此调用方只需要 await result,不需要为 dismiss 额外同步 open 状态。
11+
12+
## Goals / Non-Goals
13+
14+
**Goals:**
15+
16+
-`ProposalDetailSlideover` 取代独立详情页,保留现有 proposal 详情浏览、apply/archive、运行历史和 applying 自动恢复能力。
17+
- 删除 `/proposal/:id` 详情路由,将 proposal 列表迁移为顶层 `pages/proposal.vue`
18+
- 从 proposal 列表、Overview、Chat EventRail 以 programmatic overlay 打开详情。
19+
- 新增 proposal specs delta 只读 IPC 和共享 DTO,展示 `ADDED``MODIFIED``REMOVED``RENAMED` capability delta。
20+
- 让 Specs tab 复用 `/specs` 页左列表/右详情的信息架构,同时减少完整 spec 浏览页不适合 delta 的信息。
21+
22+
**Non-Goals:**
23+
24+
- 不重构 `useProposalRunStore` 为按 proposal 分片的多实例状态。本次继续复用现有全局 run store。
25+
- 不保留 `/proposal/:id` deep link 或兼容桥接页。
26+
- 不改 proposal apply/archive 主流程、apply run 持久化结构或 archive finalization 行为。
27+
- 不把 proposal specs delta 合并或展示为主仓库 `openspec/specs` 的完整 capability。
28+
29+
## Decisions
30+
31+
### 1. 详情作为业务 Slideover 组件,而不是页面组件复用
32+
33+
创建 `src/renderer/src/components/proposal/ProposalDetailSlideover.vue`,接收 `changeId` prop,并在组件内部承接原 `[id].vue` 的数据加载、markdown 文件读取、workflow 菜单、apply/archive、run history 与 SidePanel 状态。
34+
35+
备选方案是保留 `[id].vue` 并在 Slideover 内渲染它,但该页面强依赖 `useRoute()``router.replace()`,会把路由语义继续带进 overlay。独立组件能把输入边界收敛为 `changeId`,并让 archive 后的 id 变化通过组件状态或 overlay patch 完成。
36+
37+
### 2. 使用 composable 统一 programmatic overlay 打开方式
38+
39+
新增 `src/renderer/src/composables/useProposalDetailSlideover.ts`,显式 import `ProposalDetailSlideover` 后调用 `useOverlay().create(ProposalDetailSlideover, { destroyOnClose: true })`。入口组件只调用 `openProposalDetail(changeId)`,不直接接触 overlay 实例。
40+
41+
项目当前没有使用 `#components``Lazy*` 组件别名,已有 `useConfirmDialog` 采用显式 import + `overlay.create(Component)` 模式;本次沿用该模式,避免引入新的组件解析习惯。
42+
43+
### 3. Slideover 允许 dismiss,关闭结果不承载业务语义
44+
45+
`ProposalDetailSlideover` 使用 `USlideover` 默认 dismissible 行为,允许 ESC 与遮罩关闭。显式关闭按钮 emit `close`,dismiss 由 `v-model:open``after:leave``close(id)` 链路 resolve overlay result 为 `undefined`
46+
47+
调用方 await result 只用于等待关闭完成,不根据 result 分支执行业务逻辑。这样与 Nuxt UI 4.9 的真实实现一致,也避免为 ESC/遮罩关闭重复维护外部 open 状态。
48+
49+
### 4. Slideover 宽度固定为详情级工作面板
50+
51+
右侧 Slideover content 覆盖 Nuxt UI 默认 `max-w-md`,使用 `w-[min(100vw,1120px)] max-w-none` 或等价 class。1120px 来自当前详情主阅读区 `max-w-3xl`(约 768px)加运行日志 SidePanel `w-96`(约 384px)的组合宽度。窄窗口由 `min(100vw,1120px)` 自然退化为全宽。
52+
53+
### 5. Proposal specs delta 通过新 IPC 聚合返回
54+
55+
不复用 `proposal:readFile` 读取 `specs/<capability>/spec.md`。现有 `readChangeFile` 对 filename 使用 `basename()`,是一个刻意收窄的顶层文件读取边界。为了展示 specs delta,应新增 `proposal:getSpecDeltas`,在主进程受控扫描 `specs/*/spec.md` 并返回解析后的 `ProposalSpecDeltaOverview`
56+
57+
解析逻辑可以复用 `src/main/services/specs/specs-markdown-parser.ts` 的 heading 解析思路,但需要新增 delta section 识别:`ADDED``MODIFIED``REMOVED``RENAMED`。返回 DTO 放在 `src/shared/types/proposal.ts`,schema 放在 `src/shared/schemas/ipc/proposal.ts`
58+
59+
### 6. Specs tab 复用信息架构,不复用完整页面语义
60+
61+
Specs tab 的左侧列表和右侧 requirement/scenario 展示复用 `src/renderer/src/pages/specs.vue` 的布局语言和组件组合思路,但不要直接把 `/specs` 页面搬进详情:
62+
63+
- 不显示主 specs 页的 `updatedAt`、完整 `sourcePath` 重点信息,因为 proposal specs 是 delta。
64+
- capability 和 requirement 都显示 delta badge。
65+
- `REMOVED` / `RENAMED` 可能没有 scenario,UI 必须能展示纯正文。
66+
- 空态文案应说明“当前 proposal 没有 specs delta”,避免误解为项目没有 capability specs。
67+
68+
### 7. 路由删除后让文件系统路由自然更新
69+
70+
删除 `src/renderer/src/pages/proposal/[id].vue`,将 `src/renderer/src/pages/proposal/index.vue` 内容迁到 `src/renderer/src/pages/proposal.vue`,删除原空壳。`src/renderer/src/typed-router.d.ts` 是 vue-router 自动生成文件,不在任务里手动编辑;通过 dev/typecheck 生成链路自然更新。
71+
72+
## Risks / Trade-offs
73+
74+
- `useProposalRunStore` 是全局单例 → 在 Chat 页面上打开 Slideover 并恢复某个 proposal run history,可能改变 Chat EventRail 用于判断“可归档”的 `runMeta`。本次接受该现状,因为独立详情页已经使用同一个 store;若后续需要并发查看多个 proposal,再单独设计按 changeId 分片的 run store。
75+
- 删除 `/proposal/:id` → 外部 deep link 或历史地址不再可用。用户已明确不需要旧版本兼容;常规入口全部改为 Slideover。
76+
- Specs delta parser 与主 specs parser 相似但语义不同 → 不要把 delta parser 合并到完整 specs browser,避免主 specs 页误读 change spec 的 delta section。
77+
- Slideover 宽度较大 → 小窗口会全宽覆盖当前页面;这是桌面应用中详情工作面板的合理退化,优先保证 markdown 和日志可读性。
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
## Why
2+
3+
Proposal 详情当前作为独立页面打开,会让用户离开 Overview 或 Chat 的工作上下文;同时详情只展示 proposal/design/tasks 三个 markdown 文件,无法直接查看 change 内的 capability delta。将详情改为编程打开的 Slideover 后,用户可以在当前页面内完成浏览、apply/archive 与运行日志查看,并把 specs delta 纳入同一个详情面板。
4+
5+
## What Changes
6+
7+
- **BREAKING**: 移除 `/proposal/:id` 独立详情路由;proposal 详情不再通过路由深链访问。
8+
-`pages/proposal/index.vue` 迁移为顶层 `pages/proposal.vue`,删除只渲染 `RouterView``pages/proposal.vue` 空壳和详情子页面。
9+
- 新增独立 `ProposalDetailSlideover` 组件,并通过 `useOverlay().create(...)` 以编程方式从 proposal 列表、Overview 进行中变更、Chat EventRail 的“查看详情”入口打开。
10+
- Slideover 允许 ESC 和遮罩关闭;调用方只 await overlay result,不需要为 dismiss 场景补额外关闭状态处理。
11+
- Proposal 详情保留现有 header、Proposal/Design/Tasks tab、apply/archive 操作、运行历史 SidePanel 与 applying 自动恢复行为。
12+
- Proposal 详情新增 Specs tab,展示当前 proposal `specs/<capability>/spec.md` 中的 capability delta,而不是完整 capability spec。
13+
- 新增受控 proposal specs delta 读取 IPC,返回解析后的 delta DTO,不放宽现有 `proposal:readFile` 的顶层文件读取边界。
14+
- Specs delta UI 复用 `/specs` 浏览页的信息架构,但简化为左侧 capability delta 列表、右侧 requirement/scenario delta 内容,并用 ADDED/MODIFIED/REMOVED/RENAMED badge 表达 delta 类型。
15+
16+
## Capabilities
17+
18+
### New Capabilities
19+
20+
-
21+
22+
### Modified Capabilities
23+
24+
- `proposal-detail`: proposal 详情从独立路由改为 Nuxt UI Slideover,并新增 Specs delta tab。
25+
- `proposal-list`: proposal 列表点击行为从路由跳转改为打开详情 Slideover,并调整 `/proposal` 文件系统路由结构。
26+
- `proposal-ipc`: 新增 proposal specs delta 读取 IPC 与共享 DTO。
27+
- `app-shell-routing`: 移除 `/proposal/:id` 作为项目作用域路由的契约,保留 `/proposal` 列表页。
28+
- `chat-event-rail-proposal-status`: Chat EventRail 的详情入口改为打开详情 Slideover,不再导航到详情页。
29+
- `project-overview`: Overview 进行中变更入口改为打开详情 Slideover,`ActiveChange.id` 语义调整为打开详情所需 changeId。
30+
31+
## Impact
32+
33+
- Renderer: `src/renderer/src/pages/proposal.vue``src/renderer/src/pages/proposal/index.vue``src/renderer/src/pages/proposal/[id].vue``src/renderer/src/components/proposal/**``src/renderer/src/components/chat/event/ChatProposalPanel.vue``src/renderer/src/components/overview/OverviewActiveChanges.vue`、相关 composable 与测试。
34+
- Main / preload / shared IPC: `src/shared/types/channels.ts``src/shared/types/proposal.ts``src/shared/schemas/ipc/proposal.ts``src/main/ipc/proposal.ts``src/main/services/proposal/**``src/preload/api/proposal.ts``src/preload/index.d.ts``src/renderer/src/api/proposal.ts`
35+
- Specs parsing: 复用 `src/main/services/specs/specs-markdown-parser.ts` 的 requirement/scenario 解析思路,但增加 OpenSpec delta section 识别。
36+
- Tests: 更新旧详情页路由测试,新增 Slideover 打开/关闭、proposal specs delta 解析与 IPC、入口组件行为测试。
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
## MODIFIED Requirements
2+
3+
### 要求:非欢迎页共享路由级应用外壳
4+
5+
系统 SHALL 为所有应用页面提供路由级父页面,该父页面 SHALL 渲染带有专用 header、侧边导航和主内容区域的共享应用外壳布局。
6+
7+
#### 场景:共享外壳包裹应用页面
8+
9+
- **WHEN** 用户导航到 `/chat``/workflow``/integration``/proposal``/settings`
10+
- **THEN** 路由在共享应用外壳内渲染
11+
- **AND** 页面专属内容在外壳主区域渲染
12+
13+
### 要求:应用页面需要当前项目
14+
15+
系统 SHALL 在无当前项目时阻止访问项目作用域的应用路由,改为渲染 WelcomeView。
16+
17+
#### 场景:无项目时访问项目作用域路由
18+
19+
- **WHEN** 用户在无当前项目的情况下直接导航到 `/chat``/workflow``/integration``/proposal`
20+
- **THEN** 应用在主内容区域渲染 WelcomeView
21+
22+
### Requirement: ActivityBar 主导航不再包含提案入口
23+
24+
ActivityBar 中部主导航菜单 SHALL NOT 包含「提案」入口。提案区域(`/proposal` 列表页)SHALL 改由概览页进入,而非通过 ActivityBar 导航。ActivityBar 中部主导航菜单 SHALL NOT 包含「能力规约」或指向 `/specs` 的入口。能力规约浏览页 SHALL 改由概览页进入,而非通过 ActivityBar 导航。`/proposal``/specs` 路由本身 SHALL 保留,仍受共享外壳与项目作用域约束保护。系统 SHALL NOT 保留 `/proposal/:id` proposal 详情路由;proposal 详情由 programmatic Slideover 打开。
25+
26+
#### Scenario: 主导航不显示提案或能力规约入口
27+
28+
- **WHEN** 应用在共享外壳中渲染 ActivityBar 主导航菜单
29+
- **THEN** 菜单中不出现指向 `/proposal` 的「提案」入口
30+
- **AND** 菜单中不出现指向 `/specs` 的「能力规约」入口
31+
- **AND** 其余主导航入口(概览、对话、任务、工作流、定时、集成)正常显示
32+
33+
#### Scenario: 提案和 specs 路由仍可直接访问
34+
35+
- **WHEN** 用户通过概览页入口或直接导航到 `/proposal``/specs`
36+
- **THEN** 对应页面在共享应用外壳内正常渲染
37+
38+
#### Scenario: proposal 详情路由不存在
39+
40+
- **WHEN** 用户或代码尝试导航到 `/proposal/<changeId>`
41+
- **THEN** 应用 SHALL NOT 渲染 proposal 详情页面
42+
- **AND** 常规 UI 入口 SHALL 通过 Slideover 打开 proposal 详情
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
## MODIFIED Requirements
2+
3+
### Requirement: 不展示执行日志
4+
5+
`ChatSessionEventRail``ChatProposalPanel` SHALL 不渲染 apply/archive 的流式消息、阶段详情或工具调用日志。
6+
7+
#### Scenario: apply 运行中
8+
9+
- **GIVEN** 一个 `applying` 状态的 proposal
10+
- **WHEN** apply 正在流式执行
11+
- **THEN** `ChatProposalPanel` SHALL 仅展示“实施中”状态 badge,不展示任何 chunk 消息或阶段进度条
12+
13+
#### Scenario: 用户需要查看详细日志
14+
15+
- **GIVEN** 一个 `applying` 状态的 proposal
16+
- **WHEN** 用户点击 proposal 条目或“查看详情”入口
17+
- **THEN** 应用 SHALL 打开 proposal 详情 Slideover
18+
- **AND** 详情 Slideover 的 `ProposalApplySidePanel` SHALL 展示完整日志
19+
- **AND** 应用 SHALL NOT 导航到 `/proposal/<changeId>`
20+
21+
## ADDED Requirements
22+
23+
### Requirement: ChatProposalPanel 通过 Slideover 打开详情
24+
25+
`creating` 状态外,`ChatProposalPanel` 中每个 proposal 卡片 SHALL 展示“查看详情”按钮或等价的显式详情入口。该入口 SHALL 调用 programmatic proposal 详情 Slideover 打开函数并传入 proposal id;不得调用 `router.push('/proposal/<id>')`
26+
27+
#### Scenario: draft proposal 打开详情
28+
29+
- **GIVEN** `ChatProposalPanel` 渲染一个 `draft` proposal
30+
- **WHEN** 用户点击“查看详情”
31+
- **THEN** 应用打开 proposal 详情 Slideover
32+
- **AND** 当前 Chat route 与 active session 保持不变
33+
34+
#### Scenario: archived proposal 打开详情
35+
36+
- **GIVEN** `ChatProposalPanel` 渲染一个 `archived` proposal
37+
- **WHEN** 用户点击“查看详情”
38+
- **THEN** 应用打开 proposal 详情 Slideover,并传入 archived proposal id
39+
- **AND** 当前 Chat route 与 active session 保持不变
40+
41+
#### Scenario: creating proposal 不显示详情入口
42+
43+
- **GIVEN** `ChatProposalPanel` 渲染一个 `creating` proposal
44+
- **WHEN** 用户查看 proposal 卡片操作区
45+
- **THEN** 该卡片不显示“查看详情”按钮
46+
- **AND** 该卡片不打开详情 Slideover

0 commit comments

Comments
 (0)