diff --git a/.gitignore b/.gitignore index f64eeaad28d0..3c546b75f55f 100644 --- a/.gitignore +++ b/.gitignore @@ -270,3 +270,6 @@ ci-shards/ /.build/windows-native-benchmark/ .local/static-analysis-research/ + +!docs/issue/终端聊天视图恢复/ +!docs/issue/终端聊天视图恢复/** diff --git a/config/architecture-policies.jsonc b/config/architecture-policies.jsonc index 2878d5295438..658f21ebee29 100644 --- a/config/architecture-policies.jsonc +++ b/config/architecture-policies.jsonc @@ -6,6 +6,32 @@ { "prefix": "@renderer/", "replacement": "src/renderer/src/" } ], "policies": [ + { + "id": "native-chat-view-recovery", + "description": "Restore the existing terminal/chat view action without creating or resuming provider sessions.", + "rules": [ + { + "id": "native-chat-view-recovery-worktree-scope", + "type": "change-scope", + "comparison": "worktree", + "whenChanged": [ + "src/renderer/src/native-chat-view-recovery/**", + "docs/issue/终端聊天视图恢复/**" + ], + "allowedPaths": [ + "src/renderer/src/native-chat-view-recovery/**", + "docs/issue/终端聊天视图恢复/**", + "src/renderer/src/components/tab-bar/SortableTabContextMenu.tsx", + "src/renderer/src/components/tab-bar/SortableTabContextMenu.test.tsx", + "config/fork-features.jsonc", + "config/architecture-policies.jsonc", + "docs/issue/README.md", + ".gitignore" + ], + "message": "Change is outside terminal chat view recovery." + } + ] + }, { "id": "terminal-launch-diagnostics", "description": "Observational terminal launch tracing only; no runtime ownership or wire changes.", @@ -485,6 +511,10 @@ // diff against the upstream commit it last merged, evaluated without a sync first. "requireBaseAncestor": false, "allowedPaths": [ + "src/renderer/src/native-chat-view-recovery/**", + "docs/issue/终端聊天视图恢复/**", + "src/renderer/src/components/tab-bar/SortableTabContextMenu.tsx", + "src/renderer/src/components/tab-bar/SortableTabContextMenu.test.tsx", "src/cli/integration-builds/**", "src/renderer/src/integration-builds/**", "src/main/serve-update-handoff.ts", diff --git a/config/fork-features.jsonc b/config/fork-features.jsonc index 7bfd0be1ae71..de398f9e6412 100644 --- a/config/fork-features.jsonc +++ b/config/fork-features.jsonc @@ -22,6 +22,38 @@ // Imports every feature may take without declaring them: the design-system primitives. "dependsOnIgnore": ["src/renderer/src/components/ui/**"], "features": [ + { + "id": "native-chat-view-recovery", + "title": "终端聊天视图恢复", + "goal": "恢复已有终端标签的聊天视图切换,保留同一终端与会话绑定", + "journal": "docs/issue/终端聊天视图恢复/journal.md", + "dependsOn": [ + "src/renderer/src/i18n/i18n.ts", + "src/renderer/src/components/native-chat/native-chat-shortcut.ts" + ], + "policyId": "native-chat-view-recovery", + "ownedPaths": [ + "src/renderer/src/native-chat-view-recovery/**", + "docs/issue/终端聊天视图恢复/**" + ], + "requiredFiles": ["src/renderer/src/native-chat-view-recovery/TerminalTabViewSwitch.tsx"], + "seams": [ + { + "file": "src/renderer/src/components/tab-bar/SortableTabContextMenu.tsx", + "mustContain": "TerminalTabViewSwitch", + "why": "Expose the existing same-pane view toggle in both directions." + }, + { + "file": "src/renderer/src/components/tab-bar/SortableTabContextMenu.test.tsx", + "mustContain": "switches an eligible terminal tab into chat view", + "why": "Keep the existing menu behavior contract aligned with the restored entry." + } + ], + "tests": ["src/renderer/src/components/tab-bar/SortableTabContextMenu.test.tsx"], + "checks": [ + "pnpm exec vitest run --config config/vitest.config.ts src/renderer/src/components/tab-bar/SortableTabContextMenu.test.tsx src/renderer/src/components/tab-bar/native-chat-tab-agent-evidence.test.ts" + ] + }, { "id": "integration-builds", "title": "集成分支自动出包:同一提交的 macOS、Linux 与 Android 内测包", diff --git a/docs/issue/README.md b/docs/issue/README.md index 83dac5d684dc..bfa407ea9cb1 100644 --- a/docs/issue/README.md +++ b/docs/issue/README.md @@ -4,6 +4,7 @@ | 需求 | 目标 | 状态 | 需求文档 | 方案 | 测试用例 | 更新 | | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------- | ------------ | --------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | +| [终端聊天视图恢复](终端聊天视图恢复/journal.md) | 恢复已有终端标签的聊天视图切换,保留同一终端与会话绑定 | testing | [终端聊天视图恢复](终端聊天视图恢复/requirements/终端聊天视图恢复.md) | - | [切换与绑定](终端聊天视图恢复/tests/cases/切换与绑定.md) | 2026-10-23 | | [集成分支自动出包](集成分支自动出包/journal.md) | 同源发布 fork 内测包,并提供桌面更新清单和 Android 应用内更新 | testing | [集成分支自动出包](集成分支自动出包/requirements/集成分支自动出包.md) | [2026-10-21-orcad上游同步](集成分支自动出包/solutions/2026-10-21-orcad上游同步.md)、[Linux服务自动更新方案](集成分支自动出包/solutions/Linux服务自动更新方案.md)、[macOS自动更新签名修复方案](集成分支自动出包/solutions/macOS自动更新签名修复方案.md)、[发布页分层与可读发布说明方案](集成分支自动出包/solutions/发布页分层与可读发布说明方案.md) | [Linux服务自动更新](集成分支自动出包/tests/cases/Linux服务自动更新.md)、[自动出包](集成分支自动出包/tests/cases/自动出包.md) | 2026-10-23 | | [文档预览大小](文档预览大小/journal.md) | 扩大文档文本预览上限,不扩大普通文件读取和 paired RPC 预算 | testing | [文档预览大小](文档预览大小/requirements/文档预览大小.md) | - | [预览大小边界](文档预览大小/tests/cases/预览大小边界.md) | 2026-10-21 | | [Goal 目标模式](Goal目标模式/journal.md) | 持久目标、既有会话续跑、独立验收与目标管理 UI | testing | [Goal目标模式](Goal目标模式/requirements/Goal目标模式.md) | [Goal守卫监工与唤醒兜底修订方案](Goal目标模式/solutions/Goal守卫监工与唤醒兜底修订方案.md)、[Goal守卫监工与唤醒兜底修订方案.review](Goal目标模式/solutions/Goal守卫监工与唤醒兜底修订方案.review.md)、[Goal目标模式技术说明](Goal目标模式/solutions/Goal目标模式技术说明.md)、[Goal目标管理与交互闭环方案](Goal目标模式/solutions/Goal目标管理与交互闭环方案.md) | [Goal功能测试](Goal目标模式/tests/cases/Goal功能测试.md) | 2026-10-21 | diff --git "a/docs/issue/\347\273\210\347\253\257\350\201\212\345\244\251\350\247\206\345\233\276\346\201\242\345\244\215/journal.md" "b/docs/issue/\347\273\210\347\253\257\350\201\212\345\244\251\350\247\206\345\233\276\346\201\242\345\244\215/journal.md" new file mode 100644 index 000000000000..42418d7d4752 --- /dev/null +++ "b/docs/issue/\347\273\210\347\253\257\350\201\212\345\244\251\350\247\206\345\233\276\346\201\242\345\244\215/journal.md" @@ -0,0 +1,46 @@ +--- +title: 终端聊天视图恢复 +document_type: journal +status: testing +created: 2026-10-23 +updated: 2026-10-23 +issue: 终端聊天视图恢复 +external_ids: [] +--- + +# 终端聊天视图恢复 + +## 1. 关键文档链接 + +| 类型 | 文档 | 状态 | 说明 | +| -------- | ------------------------------------------------------- | ------------ | ------------------------------------------------ | +| 需求 | [终端聊天视图恢复](requirements/终端聊天视图恢复.md) | ready | REQ-1801~1803 | +| 交互 | - | not-required | 恢复已有菜单与快捷键 | +| 方案 | - | not-required | 接入现有 toggleViewMode;不新增会话控制链路 | +| 测试用例 | [切换与绑定](tests/cases/切换与绑定.md) | ready | 双向入口、本地、SSH、历史只读 | +| 测试执行 | [本机与SSH验收](tests/runs/2026-10-23-本机与SSH验收.md) | completed | 双向菜单、进程及 transcript 保留;正式发布待完成 | + +## 2. 决策点记录 + +- 2026-10-23:根因是标签右键菜单用 isChatView 隐藏了终端到聊天的方向;终端面板右键和快捷键已支持。恢复同一标签的现有切换,不把它替换为创建新会话或恢复 provider。 +- 界面展示由客户端持有,终端和 transcript 继续由本地或 SSH 执行主机持有;不新增协议、主机连接或配置。 + +## 3. 开发记录 + +### 2026-10-23 登记与实施(implementing) + +- 本轮目标:恢复标签入口,并校验指定历史会话绑定。 +- 完成内容:核对现有两个菜单及切换回调;建立独立功能分支和需求登记。 +- 代码或文档变更:登记、需求和验收用例;实现放在功能拥有目录。 +- 验证证据:源码核对;真实界面和自动化验证待执行。 +- 未解决问题:历史标签的实际元数据仍需核对;尚未出包安装。 +- 下一步:实现入口、完成本地与 SSH 验收及所有门禁,合入正式发包。 + +### 2026-10-23 双向菜单和历史绑定验收(testing) + +- 本轮目标:完成入口回归与真实本机、SSH 验证。 +- 完成内容:功能拥有目录中的菜单项调用已有回调,支持双向标签切换并复用跨平台快捷键与现有翻译。历史标签通过已有 hook 入口重放 transcript 已记录的完成事件,补齐 provider 元数据;未向 provider 发送消息或恢复进程。 +- 代码或文档变更:SortableTabContextMenu 接线、既有菜单回归测试、需求及登记。 +- 验证证据:26 项相关测试、四目标 pnpm tc、三项本地化、架构、fork 功能和文档、变更代码质量通过。本机与 SSH 使用正常 AppImage 的后台隔离实例,双向菜单及历史正文可见;原 handle、incarnation、shell PID 保持,测试 transcript SHA256 保持。用户历史 transcript 的大小、mtime 和 SHA256 前后一致,已有 Codex PID 4013571 保留。证据见本轮 Test Run 与 .docs/native-chat-view-ui-validation/2026-10-23/。 +- 未解决问题:代码尚未随正式包交付;网页端旧的独立 folderWorkspace 目录入口在隔离实例未显示,本轮菜单验收采用已有 folder 项目,未将该目录问题计作通过。 +- 下一步:提交、合入 fork/integration,正式安装后复核原标签菜单和更新状态。 diff --git "a/docs/issue/\347\273\210\347\253\257\350\201\212\345\244\251\350\247\206\345\233\276\346\201\242\345\244\215/requirements/\347\273\210\347\253\257\350\201\212\345\244\251\350\247\206\345\233\276\346\201\242\345\244\215.md" "b/docs/issue/\347\273\210\347\253\257\350\201\212\345\244\251\350\247\206\345\233\276\346\201\242\345\244\215/requirements/\347\273\210\347\253\257\350\201\212\345\244\251\350\247\206\345\233\276\346\201\242\345\244\215.md" new file mode 100644 index 000000000000..df02c7679441 --- /dev/null +++ "b/docs/issue/\347\273\210\347\253\257\350\201\212\345\244\251\350\247\206\345\233\276\346\201\242\345\244\215/requirements/\347\273\210\347\253\257\350\201\212\345\244\251\350\247\206\345\233\276\346\201\242\345\244\215.md" @@ -0,0 +1,30 @@ +--- +title: 终端聊天视图恢复 +document_type: requirement +status: ready +created: 2026-10-23 +updated: 2026-10-23 +issue: 终端聊天视图恢复 +--- + +# 终端聊天视图恢复 + +用户要求修好 Fartown 安装包中无法发现或使用 Chat View 的问题,并完成合入与安装验证。 + +### REQ-1801 已有终端可双向切换视图 + +优先级:P0。当前状态:实现中。 + +支持原生聊天视图的终端标签,右键菜单提供 Continue in Chat UI;聊天视图提供 Continue with Terminal Agent。两者调用已有同标签切换入口,保留终端 ID、进程和 provider session。普通 shell、不支持的 agent、缺少切换回调的标签不显示此项。切换不创建标签、不启动或恢复 provider 会话、不发送用户消息。 + +### REQ-1802 本地和 SSH 行为一致 + +优先级:P0。当前状态:实现中。 + +本地文件夹和 Git 工作树、SSH 执行主机均沿用已有能力判断、会话路由和 transcript 读取。执行主机拥有 transcript 与终端进程,客户端仅切换展示;不增加 RPC、端口、设置、文件副本或重试。旧主机维持其已有能力判断。快捷键标签复用跨平台绑定,移动端的原生聊天入口保持已有行为。 + +### REQ-1803 历史会话可读且只读校验 + +优先级:P0。当前状态:实现中。 + +对用户指定旧标签核对 Orca 的 provider session 元数据;如缺失,只用已核实的会话标识及已有绑定入口修复元数据。读取并显示已有 transcript,前后校验文件未改动;不 resume 旧 provider、不发送消息、不关闭原终端。此项是既有配置的修复验收,优先复用现有接口。 diff --git "a/docs/issue/\347\273\210\347\253\257\350\201\212\345\244\251\350\247\206\345\233\276\346\201\242\345\244\215/tests/cases/\345\210\207\346\215\242\344\270\216\347\273\221\345\256\232.md" "b/docs/issue/\347\273\210\347\253\257\350\201\212\345\244\251\350\247\206\345\233\276\346\201\242\345\244\215/tests/cases/\345\210\207\346\215\242\344\270\216\347\273\221\345\256\232.md" new file mode 100644 index 000000000000..6562a3b0d33d --- /dev/null +++ "b/docs/issue/\347\273\210\347\253\257\350\201\212\345\244\251\350\247\206\345\233\276\346\201\242\345\244\215/tests/cases/\345\210\207\346\215\242\344\270\216\347\273\221\345\256\232.md" @@ -0,0 +1,15 @@ +--- +title: 切换与绑定 +document_type: test-case +status: ready +created: 2026-10-23 +updated: 2026-10-23 +issue: 终端聊天视图恢复 +--- + +# 切换与绑定 + +- TC-1801(REQ-1801):支持的终端右键显示聊天入口,点击仅调用同标签切换一次;聊天视图可返回终端,原 handle 和 PID 保持。 +- TC-1802(REQ-1801):普通 shell、无能力或缺少回调时不显示切换;原有历史会话操作菜单仍在正确分组。 +- TC-1803(REQ-1802):本地和真实 SSH 执行端分别运行隔离 App;已有 transcript 在两种展示间切换,终端进程保留。不新增或替换 provider 会话。 +- TC-1804(REQ-1803):核对用户旧标签的 provider session ID、执行主机 transcript 路径和可读内容;前后文件大小、mtime、SHA256 一致,无 resume 或发送操作。 diff --git "a/docs/issue/\347\273\210\347\253\257\350\201\212\345\244\251\350\247\206\345\233\276\346\201\242\345\244\215/tests/runs/2026-10-23-\346\234\254\346\234\272\344\270\216SSH\351\252\214\346\224\266.md" "b/docs/issue/\347\273\210\347\253\257\350\201\212\345\244\251\350\247\206\345\233\276\346\201\242\345\244\215/tests/runs/2026-10-23-\346\234\254\346\234\272\344\270\216SSH\351\252\214\346\224\266.md" new file mode 100644 index 000000000000..8fdf8d7c1822 --- /dev/null +++ "b/docs/issue/\347\273\210\347\253\257\350\201\212\345\244\251\350\247\206\345\233\276\346\201\242\345\244\215/tests/runs/2026-10-23-\346\234\254\346\234\272\344\270\216SSH\351\252\214\346\224\266.md" @@ -0,0 +1,27 @@ +--- +title: 本机与SSH验收 +document_type: test-run +status: completed +created: 2026-10-23 +updated: 2026-10-23 +issue: 终端聊天视图恢复 +--- + +# REQ-1801~1803 / TC-1801~1804 验收 + +开发包 preview.9010 由正常 integration electron-builder 配置构建,用于隔离验证,未安装到用户服务。 + +| 项目 | 结果 | +| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------- | +| 菜单、能力门禁与同标签所有权回归 | 3 文件 26 项通过 | +| 类型检查 | pnpm tc 四目标通过 | +| 静态门禁 | fork 功能与文档、18 组架构策略、三项本地化和本分支 3 个代码文件全部增量质量扫描通过 | +| 本机 AppImage | 同一终端标签右键进入 Chat UI,显示既有测试 transcript,右键返回终端 | +| SSH AppImage | root sshd 的 Ubuntu 20.04 隔离容器,客户端 HTTP/WebSocket 均经 SSH 转发,远端文件及服务独立;完成相同双向切换 | +| 进程与会话保留 | 本机 PID 1047024、SSH PID 5344 均 live,handle 和 incarnation 一致;测试 transcript SHA256 一致 | +| 用户历史会话 | provider 01a15c57-f3c1-7b90-81e1-e5bfafd150d8;已有 Codex PID 4013571;只更新 Orca hook 元数据,实际网页 Chat View 显示已有对话 | +| 用户历史文件只读 | 大小 1023425013,mtimeNs 1792724055678743227,SHA256 a175c62c1ed8d548717533777d9afa54022da0a80c93ea2582c2a3a1584f46d4,前后一致 | + +证据目录 `.docs/native-chat-view-ui-validation/2026-10-23/`:scripts 为可复查脚本,evidence 中包含 local/ssh-result.json、双向入口与正文截图、历史绑定摘要及截图。测试模拟已有 Codex transcript 和已完成 hook,不启动真实 provider。测试夹具曾使用错误的 content 类型,按现有 decoder 契约修正后验收通过,未更改产品解析器。 + +范围限制:真实 UI 使用 folder 项目,不把网页独立 folderWorkspace 目录未显示的问题算作通过。菜单能力路由复用既有本地/SSH 所有权;未修改远端协议。macOS/Windows 快捷键标签复用既有跨平台模块,本轮没有对应平台真机证据。正式发布、覆盖安装与生产菜单验收仍待执行。 diff --git a/src/renderer/src/components/tab-bar/SortableTabContextMenu.test.tsx b/src/renderer/src/components/tab-bar/SortableTabContextMenu.test.tsx index 8f1a5a5b42d3..48a6911120f1 100644 --- a/src/renderer/src/components/tab-bar/SortableTabContextMenu.test.tsx +++ b/src/renderer/src/components/tab-bar/SortableTabContextMenu.test.tsx @@ -274,12 +274,37 @@ describe('SortableTabContextMenu', () => { expect(container.textContent).not.toContain('Continue in Chat UI') }) - it('hides switching a terminal-view tab into chat view', () => { - const { container } = renderMenu({ canToggleViewMode: true, onToggleViewMode: vi.fn() }) + it('switches an eligible terminal tab into chat view through its existing callback', () => { + const onToggleViewMode = vi.fn() + const onClose = vi.fn() + const { container, onActivate } = renderMenu({ + canToggleViewMode: true, + onToggleViewMode, + onClose + }) + const marker = getSurfaceSwitchMarker(container) - expect(container.textContent).not.toContain('Continue in Chat UI') + expect(getButton(container, 'Continue in Chat UI').compareDocumentPosition(marker)).toBe( + Node.DOCUMENT_POSITION_FOLLOWING + ) + expect(marker.getAttribute('data-leading-separator')).toBe('false') + act(() => getButton(container, 'Continue in Chat UI').click()) + expect(onToggleViewMode).toHaveBeenCalledExactlyOnceWith() + expect(onActivate).not.toHaveBeenCalled() + expect(onClose).not.toHaveBeenCalled() }) + it.each([{ canToggleViewMode: true }, { canToggleViewMode: false, onToggleViewMode: vi.fn() }])( + 'hides an unavailable view switch: %j', + (props) => { + const { container } = renderMenu(props) + + expect(container.textContent).not.toContain('Continue in Chat UI') + expect(container.textContent).not.toContain('Continue with Terminal Agent') + expect(getSurfaceSwitchMarker(container).getAttribute('data-leading-separator')).toBe('true') + } + ) + it('keeps the way back for a tab already in chat view, in the chat/CLI move section', () => { const onToggleViewMode = vi.fn() const { container } = renderMenu({ diff --git a/src/renderer/src/components/tab-bar/SortableTabContextMenu.tsx b/src/renderer/src/components/tab-bar/SortableTabContextMenu.tsx index 20bdc0255297..98866280810d 100644 --- a/src/renderer/src/components/tab-bar/SortableTabContextMenu.tsx +++ b/src/renderer/src/components/tab-bar/SortableTabContextMenu.tsx @@ -1,13 +1,4 @@ -import { - PanelLeftClose, - PanelRightClose, - Pin, - PinOff, - Pencil, - SquareTerminal, - X, - ListX -} from 'lucide-react' +import { PanelLeftClose, PanelRightClose, Pin, PinOff, Pencil, X, ListX } from 'lucide-react' import { DropdownMenu, DropdownMenuContent, @@ -26,6 +17,7 @@ import type { NativeChatTabSession } from './tab-session-history-switch' import { TAB_CONTEXT_MENU_CONTENT_CLASS } from './tab-context-menu-sizing' import { CopyTabIdMenuItem } from './CopyTabIdMenuItem' import { CloseSplitPaneMenuItem } from './CloseSplitPaneMenuItem' +import { TerminalTabViewSwitch } from '@/native-chat-view-recovery/TerminalTabViewSwitch' const TAB_COLORS = [ { @@ -162,9 +154,7 @@ export function SortableTabContextMenu({ const closeShortcut = useOptionalShortcutLabel('tab.close') const renameShortcut = useOptionalShortcutLabel('tab.rename') - // Why: switching a tab into chat view is hidden while that toggle is removed; a tab already in - // chat view keeps its way back. - const showTerminalViewSwitch = canToggleViewMode && isChatView && onToggleViewMode !== undefined + const showTerminalViewSwitch = canToggleViewMode && onToggleViewMode !== undefined return ( @@ -190,13 +180,7 @@ export function SortableTabContextMenu({ {showTerminalViewSwitch ? ( <> - - - {translate( - 'components.tab.bar.SortableTabContextMenu.switchToTerminalView', - 'Continue with Terminal Agent' - )} - + ) : null} void +}): React.JSX.Element { + const shortcut = nativeChatToggleShortcutLabel(isMacPlatform()) + return ( + + {isChatView ? ( + + ) : ( + + )} + {isChatView + ? translate( + 'components.tab.bar.SortableTabContextMenu.switchToTerminalView', + 'Continue with Terminal Agent' + ) + : translate( + 'components.tab.bar.SortableTabContextMenu.switchToChatView', + 'Continue in Chat UI' + )} + {shortcut} + + ) +}