Skip to content

Commit 2ae688a

Browse files
committed
feat(session-management): add inline session title actions
- synced session-management spec - archived optimize-chat-session-item-actions change - covered session item action behavior
1 parent 81c270a commit 2ae688a

8 files changed

Lines changed: 456 additions & 28 deletions

File tree

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-25T00:00:00.000Z
3+
status: applying
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
## Context
2+
3+
当前 `SessionItem.vue` 的更多操作菜单包含“重命名”和“删除”两个选项,但实际行为分别调用浏览器原生 `prompt("Rename session:", ...)``confirm("Are you sure...")`。这绕过了 renderer 规范中对简单确认弹窗的要求,也会让桌面应用出现不一致的系统级浏览器弹窗。
4+
5+
`useSessionStore` 已提供 `renameSession(sessionId, title)``deleteSession(sessionId)`,并通过现有 `chat:updateSession` / `chat:removeSession` IPC 同步磁盘。此次不需要新增 IPC、共享类型或持久化字段,只需要完善 `SessionItem.vue` 的交互和测试。
6+
7+
当前标题容器使用 `pr-8` 为绝对定位的三点按钮永久让位。按钮默认不可见,导致非 hover 状态下右侧出现空白,窄侧栏中标题只能显示较短内容。
8+
9+
## Goals / Non-Goals
10+
11+
**Goals:**
12+
13+
- 使用“修改标题”作为菜单文案,明确用户编辑的是 session 的展示标题。
14+
- 删除会话使用 `useConfirmDialog()`,确认后才调用 `useSessionStore.deleteSession()`
15+
- 修改标题使用应用内 UI,不再调用 `window.prompt`
16+
- 正常状态下移除标题区域的永久右侧留白,提高可见标题长度。
17+
- 通过 `test/renderer/src/components/session-item.spec.ts` 覆盖主要交互分支。
18+
19+
**Non-Goals:**
20+
21+
- 不新增或修改 chat IPC channel。
22+
- 不改变 session 标题的持久化格式、排序规则、agent 自动标题覆盖逻辑或删除文件语义。
23+
- 不调整 Chat sidebar 宽度。
24+
- 不引入新的全局弹窗组件或更新 `useConfirmDialog()` 契约。
25+
- 不更新 repository guidelines;现有 `RendererProcess.md``UiDesign.md``Testing.md` 已覆盖本次确认弹窗、布局和测试约束。
26+
27+
## Decisions
28+
29+
### 1. 菜单文案使用“修改标题”
30+
31+
采用“修改标题”而不是“重命名”。Session 的 `title` 是可由用户编辑、也可由 agent 的 `session_info_update` 覆盖的展示字段,不是不可变资源名或文件名。“修改标题”比“重命名”更贴近当前对象和行为。
32+
33+
备选方案“重命名”更短,但容易暗示资源名或 session 标识发生变化。实际实现只调用 `renameSession(sessionId, title)` 更新标题字段,不改 `sessionId`
34+
35+
### 2. 修改标题采用 session item 内就地编辑
36+
37+
选择在当前 session item 内进入编辑态,而不是打开确认弹窗或继续使用 `prompt`。菜单选择“修改标题”后,标题行切换为输入框,输入框默认填入当前标题并获得焦点;按 Enter 或 blur 提交,按 Escape 取消。提交前对输入执行 `trim()`
38+
39+
- trim 后为空时退出编辑态且不调用 `renameSession`
40+
- trim 后与当前标题一致时退出编辑态且不调用 `renameSession`
41+
- trim 后有效且不同于当前标题时调用 `useSessionStore.renameSession(session.id, trimmedTitle)`
42+
43+
这个方案让编辑对象与编辑位置保持一致,避免为了单字段文本输入新增复杂 modal。实现时需要阻止编辑区点击冒泡,避免触发 session 选择。
44+
45+
### 3. 删除确认复用 `useConfirmDialog()`
46+
47+
删除是简单二选一危险操作,符合 `RendererProcess.md``useConfirmDialog()` 的适用范围。`SessionItem.vue` 应在 `handleDelete()` 中调用:
48+
49+
- `title`: `删除会话?`
50+
- `description`: 包含当前 session 标题,并说明会话将从历史记录中永久删除且不可撤销
51+
- `confirmLabel`: `删除会话`
52+
- `confirmColor`: `error`
53+
54+
当 Promise resolve 为 `true` 时调用 `sessionStore.deleteSession(session.id)`;resolve 为 `false` 时直接返回。
55+
56+
### 4. 标题区域不再为隐藏按钮永久让位
57+
58+
移除内容容器上的固定 `pr-8`。更多操作按钮继续绝对定位在右上角,但只在 `group-hover``group-focus-within` 或 dropdown 打开时显示。按钮显示时可通过自身背景或局部渐隐遮住标题末端,确保按钮可读可点;正常状态下标题行应使用完整剩余宽度。
59+
60+
`UDropdownMenu` 支持 `v-model:open``@update:open`,组件应跟踪 `menuOpen`,在菜单打开时保持按钮可见。若实际 Nuxt UI 版本只依赖 focus-within,也要确保菜单打开期间按钮不会立即消失。
61+
62+
## Risks / Trade-offs
63+
64+
- 就地编辑态与条目点击选择可能冲突 -> 输入框容器和菜单按钮必须使用 `@click.stop`,键盘提交也要阻止默认行为。
65+
- blur 自动提交可能在用户点击 Escape 或菜单按钮时产生竞态 -> 实现应让 `cancelTitleEdit()` 标记取消态,或保证 Escape 分支不会再提交同一次编辑。
66+
- 删除确认文案包含长标题时可能过长 -> 描述中可截断或依赖确认弹窗文本换行,但确认按钮必须保持 `删除会话`
67+
- 按钮 overlay 可能遮住标题尾部 -> 这是可接受取舍,因为按钮只在用户与条目交互时出现;正常浏览列表时标题可见长度应优先。
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
## Why
2+
3+
Chat 侧栏的 session item 已经暴露更多操作菜单,但“重命名”和“删除”仍依赖浏览器原生 `prompt` / `confirm`,与当前 renderer 的 Nuxt UI 和 `useConfirmDialog()` 规范不一致。当前条目还为 hover 后才出现的三点按钮永久预留右侧空间,导致正常状态下标题可见长度明显偏短。
4+
5+
## What Changes
6+
7+
- 将 session item 菜单中的“重命名”改为“修改标题”,突出用户修改的是会话展示标题,而不是资源标识。
8+
- 实现“修改标题”交互,用户可以从更多操作菜单进入就地编辑,提交后复用现有 `useSessionStore.renameSession(sessionId, title)` 持久化标题。
9+
- 删除会话改为通过 `useConfirmDialog()` 发起危险操作确认,用户确认后才调用现有 `useSessionStore.deleteSession(sessionId)`
10+
- 优化 session item 标题与更多操作按钮布局:正常状态下标题区域不再为隐藏按钮永久保留固定右侧空白,按钮仅在 hover、focus 或菜单打开时浮现。
11+
- 补充 `SessionItem` 组件测试,覆盖修改标题、删除确认、取消分支和标题布局约束。
12+
13+
## Capabilities
14+
15+
### New Capabilities
16+
17+
-
18+
19+
### Modified Capabilities
20+
21+
- `session-management`: 补充 Session 条目的“修改标题”交互、删除确认方式,以及更多操作按钮不永久压缩标题区域的布局要求。
22+
23+
## Impact
24+
25+
- `src/renderer/src/components/chat/SessionItem.vue`: 替换浏览器原生 prompt/confirm,新增就地标题编辑状态,接入 `useConfirmDialog()`,调整标题与三点菜单布局。
26+
- `src/renderer/src/composables/useConfirmDialog.ts`: 复用现有 API,不改变 composable 契约。
27+
- `src/renderer/src/stores/session.ts`: 复用现有 `renameSession` / `deleteSession`,不新增 store action。
28+
- `src/shared/types/channels.ts``src/shared/schemas/ipc/chat.ts``src/main/ipc/chat.ts``src/preload/api/chat.ts`: 不新增或修改 IPC channel。
29+
- `test/renderer/src/components/session-item.spec.ts`: 增加组件级行为测试,并按测试规范 mock `useConfirmDialog()`
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
## MODIFIED Requirements
2+
3+
### Requirement: Session 条目支持选择和操作
4+
5+
系统 SHALL 高亮当前选中的 session,并在悬停、键盘聚焦或更多菜单打开时显示更多操作菜单(修改标题、删除)。选择 session 时 SHALL 从磁盘加载该 session 的历史消息;如果该 session 已在内存中加载过消息,则直接显示内存中的最新消息。选择 session SHALL NOT 停止、取消或失效其他 session 的运行中 stream,也 SHALL NOT 阻止其他 session 后台接收后续 stream 回调。
6+
7+
Session 条目在正常状态下 SHALL NOT 为默认隐藏的更多操作按钮永久保留固定右侧空白;标题区域 SHALL 使用前导媒体区和元信息之外的剩余横向空间进行单行截断。更多操作按钮出现时 SHALL 保持可点击和可识别,但不得通过扩大 chat sidebar 宽度实现。
8+
9+
#### Scenario: 选择 session 并加载历史消息
10+
11+
- **WHEN** 用户点击 session 条目
12+
- **THEN** 该 session 以高亮背景被选中,其历史消息从磁盘加载并显示在 Chat 区域
13+
- **AND** 若 session 元数据包含 `tokenUsage`,则恢复该值到 session 对象
14+
- **AND** 若 session 元数据的 `tokenUsage` 包含 `cost`,则恢复 `cost` 到 session 对象
15+
16+
#### Scenario: 已加载消息的 session 不重复加载
17+
18+
- **WHEN** 用户切换到一个已加载过消息的 session
19+
- **THEN** 直接显示已有消息,不重新从磁盘读取
20+
- **AND** 若该 session 在后台 stream 期间已更新内存消息,则显示该内存最新状态
21+
22+
#### Scenario: 切换到其他 session 不停止后台 stream
23+
24+
- **WHEN** session A 的 `status``running`
25+
- **AND** 用户点击 session B 条目
26+
- **THEN** `activeSessionId` 切换为 session B
27+
- **AND** session A 的运行中 stream 不被取消
28+
- **AND** session A 后续收到的 stream chunk 继续更新 session A 的内存态
29+
30+
#### Scenario: 切回后台完成的 session
31+
32+
- **WHEN** 用户从 session A 切到 session B 后,session A 在后台收到 done 并更新为 `ended`
33+
- **AND** 用户再次点击 session A
34+
- **THEN** Chat 区域显示 session A 在后台接收完成后的内存消息
35+
- **AND** session A 不因已加载而丢失后台接收的 assistant 内容
36+
37+
#### Scenario: Session 更多操作菜单
38+
39+
- **WHEN** 用户悬停在 session 条目上并点击三点菜单
40+
- **THEN** 下拉菜单出现,包含修改标题或删除 session 的选项
41+
42+
#### Scenario: Session 标题区域不为隐藏菜单按钮永久让位
43+
44+
- **WHEN** session 条目未处于 hover、focus 或菜单打开状态
45+
- **THEN** 标题区域不为隐藏的三点菜单按钮保留固定右侧 padding
46+
- **AND** 标题仍以单行省略号截断
47+
- **AND** chat sidebar 宽度不改变
48+
49+
### Requirement: Session 重命名和删除同步到磁盘
50+
51+
系统 SHALL 在用户修改 session 标题或删除 session 时,将变更同步持久化到磁盘。Session 标题修改 SHALL 使用应用内交互完成,不得调用浏览器原生 `window.prompt`。删除 session SHALL 使用全局确认弹窗 `useConfirmDialog()` 完成确认,不得调用浏览器原生 `window.confirm`
52+
53+
修改标题入口的用户可见文案 SHALL 为“修改标题”。用户提交标题时,系统 SHALL 对输入执行首尾空白裁剪;裁剪后为空或与当前标题相同的输入 SHALL NOT 调用持久化更新。裁剪后非空且不同于当前标题的输入 SHALL 调用现有 session 更新链路写入磁盘 session 元数据文件。
54+
55+
删除确认弹窗 SHALL 使用危险操作语义,确认按钮文案 SHALL 为“删除会话”。用户取消确认时 SHALL NOT 删除 session;用户确认后 SHALL 调用现有删除链路删除磁盘上的元数据文件和消息文件。
56+
57+
#### Scenario: 修改 session 标题
58+
59+
- **WHEN** 用户通过菜单选择“修改标题”
60+
- **AND** 输入不同于当前标题的非空标题并提交
61+
- **THEN** 新标题写入磁盘 session 元数据文件
62+
63+
#### Scenario: 空标题不提交
64+
65+
- **WHEN** 用户通过菜单选择“修改标题”
66+
- **AND** 提交的标题经首尾空白裁剪后为空
67+
- **THEN** 系统不调用 session 标题持久化更新
68+
69+
#### Scenario: 未变化标题不提交
70+
71+
- **WHEN** 用户通过菜单选择“修改标题”
72+
- **AND** 提交的标题经首尾空白裁剪后与当前标题相同
73+
- **THEN** 系统不调用 session 标题持久化更新
74+
75+
#### Scenario: 删除 session
76+
77+
- **WHEN** 用户通过菜单删除 session
78+
- **AND** 用户在确认弹窗中确认删除
79+
- **THEN** 磁盘上对应的元数据文件和消息文件均被删除
80+
81+
#### Scenario: 取消删除 session
82+
83+
- **WHEN** 用户通过菜单删除 session
84+
- **AND** 用户在确认弹窗中取消删除
85+
- **THEN** 系统不调用 session 删除持久化操作
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
## 1. SessionItem 交互实现
2+
3+
- [x] 1.1 修改 `src/renderer/src/components/chat/SessionItem.vue` 的菜单项:将 label 从 `重命名` 改为 `修改标题`,保留 `i-lucide-pencil` 图标,并让 `onSelect` 进入标题编辑态而不是调用 `window.prompt`
4+
- [x] 1.2 在 `SessionItem.vue` 中新增标题编辑状态与方法:`isEditingTitle``titleDraft``titleInputRef``startTitleEdit()``commitTitleEdit()``cancelTitleEdit()`;编辑态默认填入 `session.title`,输入框获得焦点并选中文本,Enter 或 blur 调用 `commitTitleEdit()`,Escape 调用 `cancelTitleEdit()`
5+
- [x] 1.3 在 `commitTitleEdit()` 中执行 `const trimmedTitle = titleDraft.trim()`;当结果为空或等于 `session.title` 时不调用 store,只退出编辑态;当结果有效且变化时调用 `sessionStore.renameSession(session.id, trimmedTitle)`
6+
- [x] 1.4 替换 `handleDelete()`:导入并调用 `useConfirmDialog()`,使用 `title: "删除会话?"``confirmLabel: "删除会话"``confirmColor: "error"`,仅当确认结果为 `true` 时调用 `sessionStore.deleteSession(session.id)`;删除 `window.confirm` 用法。
7+
- [x] 1.5 确保编辑输入框、菜单按钮和 dropdown 内容使用 `@click.stop` 或等价方式阻止冒泡,不触发 `handleSelect()`;选择 session 的既有行为和后台 stream 不取消语义保持不变。
8+
9+
## 2. SessionItem 布局调整
10+
11+
- [x] 2.1 移除 `SessionItem.vue` 中标题内容容器的永久 `pr-8`,让非 hover / 非 focus / 菜单未打开状态下的 `data-test="session-title"` 使用完整剩余宽度。
12+
- [x] 2.2 保持三点菜单按钮绝对定位在条目右上角,并仅在 `group-hover``group-focus-within` 或 dropdown 打开时可见;如果 Nuxt UI `UDropdownMenu` 支持 `v-model:open`,新增 `menuOpen` 状态以保证菜单打开期间按钮保持可见。
13+
- [x] 2.3 给三点菜单触发按钮补充 `aria-label="会话操作"` 或等价可访问名称,并保持 icon-only 按钮的尺寸稳定。
14+
15+
## 3. 测试
16+
17+
- [x] 3.1 更新 `test/renderer/src/components/session-item.spec.ts`:移除对全局 `prompt` / `confirm` 的依赖,mock `@renderer/composables/useConfirmDialog` 返回可控的 `Promise<boolean>`
18+
- [x] 3.2 增加“修改标题”测试:点击 dropdown item 后显示标题输入框,提交变化后的标题会调用 `renameSession(sessionId, trimmedTitle)`
19+
- [x] 3.3 增加修改标题的空值、未变化和取消测试:空白输入、trim 后未变化标题、Escape 取消都不得调用 `renameSession`
20+
- [x] 3.4 增加删除测试:确认弹窗 resolve 为 `true` 时调用 `deleteSession(sessionId)`,resolve 为 `false` 时不调用。
21+
- [x] 3.5 增加布局断言:session 标题内容容器不再包含永久 `pr-8`,菜单触发按钮仍存在并可通过 hover/focus 类显示。
22+
23+
## 4. 验证
24+
25+
- [x] 4.1 运行 `pnpm vitest run test/renderer/src/components/session-item.spec.ts`
26+
- [x] 4.2 运行 `pnpm vitest run test/renderer/src/**/*.{test,spec}.{ts,vue}`
27+
- [x] 4.3 运行 `pnpm typecheck:web`

openspec/specs/session-management/spec.md

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,9 @@ session 列表的视觉编排 SHALL 更接近块状导航项,而不是依赖
7171

7272
### Requirement: Session 条目支持选择和操作
7373

74-
系统 SHALL 高亮当前选中的 session,并在悬停时显示更多操作菜单(重命名、删除)。选择 session 时 SHALL 从磁盘加载该 session 的历史消息;如果该 session 已在内存中加载过消息,则直接显示内存中的最新消息。选择 session SHALL NOT 停止、取消或失效其他 session 的运行中 stream,也 SHALL NOT 阻止其他 session 后台接收后续 stream 回调。
74+
系统 SHALL 高亮当前选中的 session,并在悬停、键盘聚焦或更多菜单打开时显示更多操作菜单(修改标题、删除)。选择 session 时 SHALL 从磁盘加载该 session 的历史消息;如果该 session 已在内存中加载过消息,则直接显示内存中的最新消息。选择 session SHALL NOT 停止、取消或失效其他 session 的运行中 stream,也 SHALL NOT 阻止其他 session 后台接收后续 stream 回调。
75+
76+
Session 条目在正常状态下 SHALL NOT 为默认隐藏的更多操作按钮永久保留固定右侧空白;标题区域 SHALL 使用前导媒体区和元信息之外的剩余横向空间进行单行截断。更多操作按钮出现时 SHALL 保持可点击和可识别,但不得通过扩大 chat sidebar 宽度实现。
7577

7678
#### Scenario: 选择 session 并加载历史消息
7779

@@ -104,7 +106,14 @@ session 列表的视觉编排 SHALL 更接近块状导航项,而不是依赖
104106
#### Scenario: Session 更多操作菜单
105107

106108
- **WHEN** 用户悬停在 session 条目上并点击三点菜单
107-
- **THEN** 下拉菜单出现,包含重命名或删除 session 的选项
109+
- **THEN** 下拉菜单出现,包含修改标题或删除 session 的选项
110+
111+
#### Scenario: Session 标题区域不为隐藏菜单按钮永久让位
112+
113+
- **WHEN** session 条目未处于 hover、focus 或菜单打开状态
114+
- **THEN** 标题区域不为隐藏的三点菜单按钮保留固定右侧 padding
115+
- **AND** 标题仍以单行省略号截断
116+
- **AND** chat sidebar 宽度不改变
108117

109118
### Requirement: 草稿态首条消息懒创建并持久化 session
110119

@@ -172,18 +181,42 @@ session 列表的视觉编排 SHALL 更接近块状导航项,而不是依赖
172181

173182
### Requirement: Session 重命名和删除同步到磁盘
174183

175-
系统 SHALL 在用户重命名或删除 session 时,将变更同步持久化到磁盘。
184+
系统 SHALL 在用户修改 session 标题或删除 session 时,将变更同步持久化到磁盘。Session 标题修改 SHALL 使用应用内交互完成,不得调用浏览器原生 `window.prompt`。删除 session SHALL 使用全局确认弹窗 `useConfirmDialog()` 完成确认,不得调用浏览器原生 `window.confirm`
176185

177-
#### Scenario: 重命名 session
186+
修改标题入口的用户可见文案 SHALL 为“修改标题”。用户提交标题时,系统 SHALL 对输入执行首尾空白裁剪;裁剪后为空或与当前标题相同的输入 SHALL NOT 调用持久化更新。裁剪后非空且不同于当前标题的输入 SHALL 调用现有 session 更新链路写入磁盘 session 元数据文件。
178187

179-
- **WHEN** 用户通过菜单重命名 session
188+
删除确认弹窗 SHALL 使用危险操作语义,确认按钮文案 SHALL 为“删除会话”。用户取消确认时 SHALL NOT 删除 session;用户确认后 SHALL 调用现有删除链路删除磁盘上的元数据文件和消息文件。
189+
190+
#### Scenario: 修改 session 标题
191+
192+
- **WHEN** 用户通过菜单选择“修改标题”
193+
- **AND** 输入不同于当前标题的非空标题并提交
180194
- **THEN** 新标题写入磁盘 session 元数据文件
181195

196+
#### Scenario: 空标题不提交
197+
198+
- **WHEN** 用户通过菜单选择“修改标题”
199+
- **AND** 提交的标题经首尾空白裁剪后为空
200+
- **THEN** 系统不调用 session 标题持久化更新
201+
202+
#### Scenario: 未变化标题不提交
203+
204+
- **WHEN** 用户通过菜单选择“修改标题”
205+
- **AND** 提交的标题经首尾空白裁剪后与当前标题相同
206+
- **THEN** 系统不调用 session 标题持久化更新
207+
182208
#### Scenario: 删除 session
183209

184210
- **WHEN** 用户通过菜单删除 session
211+
- **AND** 用户在确认弹窗中确认删除
185212
- **THEN** 磁盘上对应的元数据文件和消息文件均被删除
186213

214+
#### Scenario: 取消删除 session
215+
216+
- **WHEN** 用户通过菜单删除 session
217+
- **AND** 用户在确认弹窗中取消删除
218+
- **THEN** 系统不调用 session 删除持久化操作
219+
187220
### Requirement: Session 加载恢复可用命令
188221

189222
系统 SHALL 在从磁盘 session meta 加载 session 列表或 session 信息时,恢复持久化的 agent 可用命令列表与 ACP session 级 `configOptions`。主进程读取 session meta 的 `available_commands``config_options` 字段后 SHALL 通过 IPC 返回为 `Session.availableCommands``Session.configOptions`,renderer SHALL 在 `normalizeSession` 后保留这两个字段;缺失字段时返回 `undefined`,renderer 不抛错。

0 commit comments

Comments
 (0)