diff --git a/doc/prd/workbench.md b/doc/prd/workbench.md index c66e8cd9..3c9ae88d 100644 --- a/doc/prd/workbench.md +++ b/doc/prd/workbench.md @@ -602,7 +602,7 @@ Composer 使用 18px 圆角、15px 正文和 24px 行高;工具栏采用 12px - 系统行为: - 文件预览态与修改文件工作区共用 44px 紧凑头部,但主体使用不同渲染组件;头部使用“目录 > 文件”面包屑,顶层显示文件地址的目录名或文件名。 - 支持差异渲染的修改文件在面包屑旁展示当前模式 Tag:“审核”表示展示修改差异,“预览”表示展示修改后的干净文件;Tag 旁的切换图标负责在两种模式间切换。差异数量、修改状态和回退按钮只在审核模式展示,预览模式不得展示 `+N -N`、“已回退 / 已删除”或回退按钮。 - - “打开方式”组合按钮展示当前生效的默认应用名称和图标:优先使用馆长内按扩展名保存的偏好,否则跟随系统默认。下拉展示系统关联应用,点击名称仅本次打开,独立的“设为默认”只保存偏好;“跟随系统默认”清除馆长偏好。偏好跨伙伴和工作空间生效、重启保留,设置后立即同步相关面板,不修改系统关联。确认偏好应用不可用时清除偏好并提示,查询失败保留偏好,并在列表底部使用分割线隔开放置纯文字“打开所在文件夹”。 + - “打开方式”组合按钮展示当前生效的默认应用名称和图标:优先使用馆长内按扩展名保存的偏好,否则跟随系统默认。下拉展示系统关联应用,应用行只负责本次打开,生效应用仅显示淡灰色“默认”标记;应用较多时仅应用列表滚动,底部设置与文件夹入口始终可见;“默认打开应用…”在同一浮层进入选择页,标题提供返回入口,选择应用只保存偏好并返回列表,不打开文件。“使用系统设置”清除馆长偏好;不常驻显示扩展名和生效范围说明。偏好跨伙伴和工作空间生效、重启保留,设置后立即同步相关面板,不修改系统关联。确认偏好应用不可用时清除偏好并提示,查询失败保留偏好,并在列表底部使用分割线隔开放置纯文字“打开所在文件夹”。 - 当前文件为 `.docx` / `.pptx` / `.xlsx` 且打开方式列表含 WPS 时,组合按钮左侧显示问号图标,悬停提示“首次用 WPS 打开时,如弹出加载项授权,允许后 WPS 将会自动刷新显示馆长更改后的内容。”;其它格式或未检测到 WPS 时不显示。 - 文件列表入口使用文件夹图标;普通预览和修改文件工作区中的文件列表从工具栏底部开始贴右悬浮,不挤压预览宽度,也不得覆盖工具栏交互。点击面板外部、按 Escape 或再次点击文件列表按钮关闭悬浮列表;选择文件打开预览后默认保持列表展开,保留目录展开、筛选、滚动和选择状态。“打开文件”初始空页面继续使用常驻文件列表。 - 尚未添加文件目录时,“打开文件”页合并为占满 Tab 内容区的统一空态,不再并排展示打开文件说明和文件列表提示。空态使用线性文件夹图标、标题“尚未添加文件目录”、说明“在工作空间中添加目录,即可在这里浏览和打开文件。”和“前往设置”按钮;按钮进入设置的工作空间文件页,跳转前保存当前伙伴的草稿与资源 Tab,返回工作台后恢复原活动 Tab 并重新读取目录。普通文件预览中的悬浮列表复用同一空态,不替换预览内容。目录信息加载时显示加载状态;已配置目录但根目录没有条目时显示“目录中暂无文件”;文件浏览不依赖向量模型或索引完成。 @@ -627,7 +627,7 @@ Composer 使用 18px 圆角、15px 正文和 24px 行高;工具栏采用 12px - `WB-PANEL-003-AC12` 文本 Diff 行背景至少覆盖整个可见画布宽度;长行产生横向滚动时,新增、删除和上下文背景应延伸到完整可滚动宽度,不得在文件列表前留下无意义白块。 - `WB-PANEL-003-AC13` 从修改文件入口进入修改文件工作区时,工作区文件列表默认关闭;用户点击文件夹图标后,列表从 44px 工具栏底部开始贴右悬浮并默认进入“本次运行改动”。悬浮层不得改变预览宽度或覆盖工具栏,点击外部、按 Escape 或再次点击按钮关闭;选择文件打开普通预览或审核后保持展开、筛选、滚动和选择状态。点击文件总分类或其历史记录时,必须按所选记录重新进入默认模式;未回退记录进入审核模式,已回退记录进入预览模式,即使所选记录 ID 与当前记录相同也必须执行重置。打开“全部”列表时必须重新读取磁盘目录(含已展开子目录),不得继续展示上次缓存的过期条目;仅当列表处于展开状态时,窗口重新获得焦点或恢复可见、以及 AI Run 结束才会同样刷新已加载目录;列表收起时不得因窗口焦点或 AI Run 结束触发目录刷新。 - `WB-PANEL-003-AC14` 文件在当前伙伴内存在可审核的修改记录时,无论通过普通文件入口还是修改入口打开,面包屑旁展示“审核 / 预览”模式 Tag 及切换图标;`docx`、`pptx`、`xlsx` 和普通 UTF-8 文本文件均支持该切换。普通文本的预览模式打开 当前正式路径的真实本地文件:若为可编辑 Markdown/UTF-8 文本则进入与普通打开一致的应用内编辑态;审核模式使用结构化文本 Diff;`xlsx` 的预览模式展示正式工作簿,审核模式展示带单元格边框标记的工作簿。普通文件入口默认进入预览;对话“修改的文件”、本次运行改动或历史记录入口,每次点击都按所选批次进入审核,即使同一记录已打开并切到预览;已回退记录默认预览。同一文件复用原 Tab,单纯切换 Tab 保留该记录当前模式,后台刷新不重置模式。已打开的普通预览在 AI 运行收尾、审核记录生成后自动出现切换入口,保留当前 Tab 和阅读位置。不同文件独立关联,最新有效批次按文件匹配;明确打开的历史批次不因后续运行收尾自动切换。切入预览或回退后,差异数量、修改状态和回退按钮全部隐藏。 - - `WB-PANEL-003-AC15` 打开方式列表来自操作系统,生效默认应用排在首项,馆长偏好标记“馆长默认”,跟随系统时标记“默认”;其他关联应用去重后展示;只能使用系统查询到的应用,不能任意指定应用路径。查询失败明确报错并保留馆长偏好,不误用其他应用打开。 + - `WB-PANEL-003-AC15` 打开方式列表来自操作系统,生效默认应用排在首项并统一标记“默认”;其他关联应用去重后展示。默认设置经独立入口进入同一浮层选择页,选项以勾选表示当前偏好,应用行仅本次打开;只能使用系统查询到的应用,不能任意指定应用路径。查询失败明确报错并保留馆长偏好,不误用其他应用打开。 - `WB-PANEL-003-AC16` 文件工具栏不单独展示“打开所在文件夹”按钮;该入口继续放在打开系统应用的下拉列表底部。工作区文件列表使用独立文件夹图标,收起时为透明按钮,展开后保持浅灰背景、边框和圆角,收起后选中样式立即消失。 - `WB-PANEL-003-AC18` 右侧处于预览模式且展示真实本地文件时,必须监听当前激活文件的外部修改;路径不区分工作地址根目录、子目录或伙伴运行附件目录,范围覆盖所有当前支持应用内预览的文件类型,包括文本与其他可预览格式:PDF、DOCX、PPTX、XLSX/XLSM/XLTX/XLTM、PNG/JPG/JPEG/GIF/WebP/BMP/SVG、Markdown,以及严格 UTF-8 的 TXT、LOG、代码、JSON、HTML 等文件。仅当文件内容相对上次预览发生变化时才自动读取最新磁盘内容并提示“检测到当前文件被外部修改,已自动刷新”;仅打开外部应用、锁文件或 mtime/ctime 变化而正文不变时不得刷新或提示。切换文件、关闭 Tab 或切换到审核模式后停止旧监听,窗口重新获得焦点或恢复可见时补检查;AI Run 执行期间及结束后短暂保护期内的写入只刷新不提示。Office 审核使用固定快照,不监听或刷新源文件;文本审核右侧使用当前文件并沿用文本编辑的外部修改协调规则;修改记录切换到预览模式时必须展示并监听正式路径的真实当前文件,而不是修改时快照。 - `WB-PANEL-003-AC19` 任意受支持文件类型的 AI 修改记录处于 `applied` 状态时,通过系统默认应用或指定关联应用打开成功后,提示“如果外部应用仍显示旧内容,请关闭该文档后重新打开。”;该提示不按 TXT、DOCX、PPTX 或 XLSX 等扩展名区分。普通文件、失败记录、已回退记录和打开失败场景不得显示该提示。WPS 加载项已连接时仍可出现该提示,不作为失败;实际刷新由 `WB-APPLY-002-AC8` 负责。 @@ -858,7 +858,7 @@ Composer 使用 18px 圆角、15px 正文和 24px 行高;工具栏采用 12px - `WB-EDIT-001-AC13` 加载失败的图片(缺失本地文件、失效 `https` 等)双击不打开预览,并 toast 提示「图片无法加载」。 - `WB-EDIT-001-AC14` `http(s)` 图片可直接显示;加载失败时按 AC13 处理。 -- 文本审核提供上一处/下一处、同步滚动、自动换行、未变内容折叠及单块恢复。右侧编辑实时更新审核区差异计数,不覆盖原始 AI 计数,不生成修改记录,单块恢复可撤销且不改变整条记录状态。切换模式、记录、关闭和回退前等待保存,失败保留内容。当前文件不存在时禁止写入,不自动重建。 +- 文本审核顶部为单行紧凑栏,左侧“本次运行前”、右侧“当前文件”,不常驻展示只读/可编辑说明;右端使用 28px 无常驻边框的上一处/下一处差异和展开/折叠图标按钮,悬停说明操作、键盘焦点清晰可见。未变内容提示为“已折叠 N 行”,可单击或通过键盘展开,单块恢复提示为“恢复这处修改”。文本审核提供同步滚动、自动换行、未变内容折叠及单块恢复。右侧编辑实时更新审核区差异计数,不覆盖原始 AI 计数,不生成修改记录,单块恢复可撤销且不改变整条记录状态。切换模式、记录、关闭和回退前等待保存,失败保留内容。当前文件不存在时禁止写入,不自动重建。 - `WB-EDIT-001-AC15` 双栏修改、撤销、单块恢复、换行和联动滚动正确;不同 run 的左侧基线独立,右侧始终是当前文件。 - `WB-EDIT-001-AC16` 自动保存保留完整内容、BOM 和换行风格;切换与回退前等待保存,失败保留可重试内容。 - `WB-EDIT-001-AC17` 差异计算超时提示并暂停自动比较,保留完整文本编辑保存及重试;文件缺失不重建。 diff --git a/doc/test-plan/workbench-test.md b/doc/test-plan/workbench-test.md index 7613b61a..4641d0d2 100644 --- a/doc/test-plan/workbench-test.md +++ b/doc/test-plan/workbench-test.md @@ -548,7 +548,7 @@ pnpm test:update:e2e - How:创建伙伴并添加文件地址后,从文件列表连续打开真实 pdf、docx、md、txt 文件;检查预览内容和工具条;展开打开方式菜单,确认触发系统关联应用查询、存在默认应用标记和至少一个系统返回的 Handler;docx / xlsx 在打开方式含 WPS 时工具栏问号可见,pdf / md / txt / xlsm 不可见;确认产生四个文件 Tab,再切回 Markdown Tab;从文件列表打开 `xlsx/xlsm/xltx/xltm`,关闭保持展开的悬浮文件列表后,对 `xlsx` 尝试进入单元格编辑并输入,用原生复制核对内容仍保持原值,再切 Sheet 和拖列宽。在隔离工作簿加入无缓存及过期缓存公式,通过组件只读权限保留选区与复制,使用真实网格选中与复制读取显示结果,包含零值和跨 Sheet 引用。工作簿同时包含足够的行,通过真实鼠标拖选 C17:C20,检查 AI 引用浮层可见且位于预览内,输入想法后添加到 Composer,核对工作表、范围和四行快照;滚动后从 Composer 回跳编辑并删除引用,再切 Sheet 验证单格引用入口。只读读取 DOM 区分表格主画布与内嵌文字编辑器画布,不用任意第一个 canvas 作为网格。 - Oracle:加号文件标签、目录懒加载、PDF 命中定位、Markdown / UTF-8 文本预览、Excel 只读且可浏览、选中态、面包屑、系统打开方式与工具栏行为整体对齐 `WB-PANEL-003`、`WB-EXCEL-002-AC1`;WPS 问号提示对齐 `WB-PANEL-003-AC22`;各文件分别保留且旧 Tab 可恢复;从“打开文件”选择器选中文件后选择器 Tab 被文件 Tab 原位替换、不残留,对齐 `WB-PANEL-001-AC1`。目录打开刷新由 `workspace-file-explorer-open-refresh` 覆盖。普通预览 FORCED 计算无缓存与过期缓存公式,结果显示准确,打开预览不写回源文件。多行区域和跨 Sheet 单格都能显示 AI 引用入口,确认后范围与快照准确,滚动后 Composer 回跳、编辑和删除可用;对齐 `WB-DOCUMENT-REFERENCE-002-AC1`。 -- 默认应用补充验收:通过真实菜单设置默认、一次性打开、恢复跟随系统默认,跨 Tab/伙伴及重启恢复,覆盖 WB-PANEL-003 的馆长偏好规则;应用失效与查询失败按真实环境人工补验,不注入系统关联。 +- 默认应用补充验收:应用行名称实际绘制在行内且不被裁掉,应用较多时列表滚动且底部设置与文件夹入口仍在浮层可见范围内,应用行仅本次打开,生效应用仅显示“默认”,无逐行设置按钮;经“默认打开应用…”进入同一浮层选择页,标题可返回,选择只保存偏好并返回列表,不触发外部打开,选择“使用系统设置”清除偏好;不常驻显示扩展名及生效范围说明。通过真实菜单设置默认、一次性打开、恢复系统设置,跨 Tab/伙伴及重启恢复,覆盖 WB-PANEL-003 的馆长偏好规则;应用失效与查询失败按真实环境人工补验,不注入系统关联。 #### `workspace-file-explorer-open-refresh` @@ -599,7 +599,7 @@ pnpm test:update:e2e - How:先打开 Markdown(进入编辑态);再通过真实 AI 调用 `replaceAll` 和 `edit` 修改该文件,等待 run 完成但不点击修改项,确认 assistant 修改列表已经显示非零 `+/-` 数量;检查编辑态静默刷新为最新内容、原 Tab 和文档视图保留,新增切换入口但仍在预览;从“全部”重新打开该文件确认带切换入口,再切审核检查 Diff;保持审核并发送不修改文件的后续提问,确认该历史批次不会被新运行清空;切换伙伴再切回,不点击修改项确认数量仍为非零;再点击 assistant 修改入口检查 `TextFileDiffViewer`、工具栏没有独立定位按钮且系统应用下拉包含“打开所在文件夹”;通过 E2E no-op 外部启动成功分支检查重新打开提醒,最后执行回退。 测试进程在审核打开后真实写入 C,切到预览检查 C、切回审核检查当前 C、左侧仍为 A,并展开完整未变行;回退验证后续修改确认。 - Oracle:覆盖 `WB-EDIT-001-AC15/AC16/AC18`,双栏当前文件编辑保存和撤销、原始 AI 记录保持不变;AI 写入后编辑态显示最新内容且不误报外部修改;不点击修改项即出现模式切换、默认仍为预览且不重挂文档视图;从“全部”打开已修改文件仍可切审核,伙伴恢复后关联记录保持;编辑态与审核模式共用一个文件 Tab;Run 终态后修改入口在未点击时已显示真实非零数量;切换伙伴再切回后未点击时数量仍为真实非零,不重置为 0,对齐 `WB-APPLY-005-AC4`;审核模式展示差异数量、修改状态和回退,预览模式全部隐藏;预览模式对 Markdown/UTF-8 文本进入应用内编辑态(与普通打开一致),对齐 `WB-EDIT-001-AC7`、`WB-PANEL-003-AC14`;已应用记录打开外部应用后显示重新打开提醒;文件夹入口打开的文件列表从工具栏底部贴右悬浮,不改变预览宽度,可通过外部点击、Escape 或选择文件关闭;当前已经是最新记录且处于预览模式时,再次点击文件总分类仍会重置到最新审核模式;历史批次、结构化 Diff、不可变展开和回退刷新整体对齐 `WB-PANEL-003`、`WB-APPLY-001`、`WB-APPLY-002`、`WB-APPLY-003`;确认回退后 bytes 与原始输入一致且有效改动标记消失。 -- 双栏补充验收:真实 Monaco 左侧 A 只读、右侧 C 可编辑,输入/撤销/单块恢复/同步滚动/折叠/换行;保存后磁盘含完整内容,保留 BOM 和 CRLF,切换模式/记录前 flush;原始 AI 计数不变,对齐 WB-EDIT-001-AC7/AC15/AC16/AC18。大文件超时、文件缺失及保存失败列入真实人工验收,对齐 AC17。 +- 双栏补充验收:顶部标题与图标操作同一行,左右标题为“本次运行前/当前文件”,无常驻只读/可编辑文案;上一处/下一处差异及折叠图标具备准确可访问名称。未变行显示中文“已折叠 N 行”,支持单击展开,恢复箭头提示中文。真实 Monaco 左侧 A 只读、右侧 C 可编辑,输入/撤销/单块恢复/同步滚动/折叠/换行;保存后磁盘含完整内容,保留 BOM 和 CRLF,切换模式/记录前 flush;原始 AI 计数不变,对齐 WB-EDIT-001-AC7/AC15/AC16/AC18。大文件超时、文件缺失及保存失败列入真实人工验收,对齐 AC17。 #### `ppt-create-diff-and-revert` @@ -666,7 +666,7 @@ pnpm test:update:e2e - 测试类型:`e2e / exclusive`(`result/wps-office/`,真实已授权组件)。 - What:Office 审核点击打开 WPS 并定位当前文件,覆盖未打开与已打开两次点击。 - Why:仅验证启动或写回刷新不能证明差异区域定位成功。 -- How:隔离伙伴通过真实 UI 选择完全访问,在工作区注册与索引前给三类临时输入添加本轮唯一文件名,避免表格恢复窗口的同名冲突;WPS 关闭时由真实 AI 修改三类文件;设置馆长默认 WPS,通过审核区域点击启动并读取真实加载项选择回报,再次点击验证已打开路径。每个组件分别记录授权与失败原因,未授权明确跳过;不会通过内部 IPC 触发被测导航,读取索引和状态仅收集证据。Excel 按真实审核工作簿的行高列宽计算点击位置,并通过拖动分隔条保证目标可见;PPT 点击审核 SVG 中与索引对应的实际元素身份。macOS 演示通过实际 SlideID 校验,表格打开时重算造成的未保存状态不得阻止可唯一确认的单元格定位。 +- How:隔离伙伴通过真实 UI 选择完全访问,在工作区注册与索引前给三类临时输入添加本轮唯一文件名,避免表格恢复窗口的同名冲突;WPS 关闭时由真实 AI 修改三类文件;通过“默认打开应用…”选择页设置馆长默认 WPS,通过审核区域点击启动并读取真实加载项选择回报,再次点击验证已打开路径。每个组件分别记录授权与失败原因,未授权明确跳过;不会通过内部 IPC 触发被测导航,读取索引和状态仅收集证据。Excel 按真实审核工作簿的行高列宽计算点击位置,并通过拖动分隔条保证目标可见;PPT 点击审核 SVG 中与索引对应的实际元素身份。macOS 演示通过实际 SlideID 校验,表格打开时重算造成的未保存状态不得阻止可唯一确认的单元格定位。 - Oracle:`WB-WPS-001-AC6/AC7`:已授权组件必须给出当前文件与实际选区的 located/container 回报,两次导航不得写入文件,也不得触发 close/reload;未授权组件不伪造覆盖。未保存编辑、多实例及位置失效由协议 Unit 补证与真机人工验收。 #### `wps-office-reload-on-write-and-revert` diff --git a/package.json b/package.json index 4726a8e5..963023d2 100644 --- a/package.json +++ b/package.json @@ -168,7 +168,8 @@ "pnpm": { "patchedDependencies": { "pptx-kit-preview@0.6.2": "patches/pptx-kit-preview@0.6.2.patch", - "@js-preview/docx@1.6.4": "patches/@js-preview__docx@1.6.4.patch" + "@js-preview/docx@1.6.4": "patches/@js-preview__docx@1.6.4.patch", + "monaco-editor@0.57.0": "patches/monaco-editor@0.57.0.patch" } } } diff --git a/patches/monaco-editor@0.57.0.patch b/patches/monaco-editor@0.57.0.patch new file mode 100644 index 00000000..2a6f51a5 --- /dev/null +++ b/patches/monaco-editor@0.57.0.patch @@ -0,0 +1,16 @@ +diff --git a/esm/vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature.js b/esm/vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature.js +index c7e609cf08f2c95b6be749b9474dd0b2bcee33ee..62228ce6301d0f09840b60ab68f1ab6eaad9b183 100644 +--- a/esm/vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature.js ++++ b/esm/vs/editor/browser/widget/diffEditor/features/hideUnchangedRegionsFeature.js +@@ -443,9 +443,9 @@ let CollapsedCodeOverlayWidget = class CollapsedCodeOverlayWidget extends ViewZo + const isFullyRevealed = this._isFullyRevealed(reader); + const lineCount = _unchangedRegion.getHiddenModifiedRange(reader).length; + const linesHiddenText = localize(146, '{0} hidden lines', lineCount); +- const span = $('span', this._compactControl ? undefined : { title: localize(147, 'Double click to unfold') }, this._compactControl && isFullyRevealed ? localize(148, 'Collapse unchanged lines') : linesHiddenText); ++ const span = $(this._compactControl ? 'span' : 'button', this._compactControl ? undefined : { type: 'button', class: 'unchanged-lines-label', title: localize(147, 'Click to unfold') }, this._compactControl && isFullyRevealed ? localize(148, 'Collapse unchanged lines') : linesHiddenText); + if (!this._compactControl) { +- reader.store.add(addDisposableListener(span, EventType.DBLCLICK, e => { ++ reader.store.add(addDisposableListener(span, EventType.CLICK, e => { + if (e.button !== 0) { + return; + } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 6f8f2573..c5bb0526 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ patchedDependencies: '@js-preview/docx@1.6.4': hash: ct5vr5jhsv3bucursjwzv64g4a path: patches/@js-preview__docx@1.6.4.patch + monaco-editor@0.57.0: + hash: picefr2elbd4zt7g46w7bc7rrq + path: patches/monaco-editor@0.57.0.patch pptx-kit-preview@0.6.2: hash: lwzt4hp3vfourxarnxhjleevzi path: patches/pptx-kit-preview@0.6.2.patch @@ -162,7 +165,7 @@ importers: version: 2.0.1 monaco-editor: specifier: 0.57.0 - version: 0.57.0 + version: 0.57.0(patch_hash=picefr2elbd4zt7g46w7bc7rrq) node-pty: specifier: 1.2.0-beta.15 version: 1.2.0-beta.15 @@ -14932,7 +14935,7 @@ snapshots: mkdirp@1.0.4: {} - monaco-editor@0.57.0: + monaco-editor@0.57.0(patch_hash=picefr2elbd4zt7g46w7bc7rrq): dependencies: dompurify: 3.4.15 marked: 14.0.0 diff --git a/src/renderer/src/assets/base.css b/src/renderer/src/assets/base.css index 8c648be5..fb49f26a 100644 --- a/src/renderer/src/assets/base.css +++ b/src/renderer/src/assets/base.css @@ -244,26 +244,76 @@ textarea, margin: 4px 8px; } -.workbench-open-with-menu .ant-dropdown-menu { +.workbench-open-with-menu .open-with-popup { width: 260px; - min-width: 0; max-width: calc(100vw - 16px); - overflow: hidden; padding: 6px; + background: #fff; + border: 1px solid #eeeeec; + border-radius: 8px; + box-shadow: var(--shadow-lg); } -.workbench-open-with-menu .ant-dropdown-menu-item { - min-height: 38px; +.workbench-open-with-menu .ant-menu { + border: 0 !important; + background: transparent; + font-size: 13px; +} + +.workbench-open-with-menu .open-with-applications > .ant-menu-item-group-title { + display: none; +} + +.workbench-open-with-menu .open-with-applications > .ant-menu-item-group-list { + max-height: 272px; + margin: 0; + overflow-y: auto; +} + +.workbench-open-with-menu .ant-menu-item { display: flex; align-items: center; gap: 10px; - padding: 7px 10px; - min-width: 0; + width: 100%; + height: 34px; + margin: 0; + padding-inline: 10px !important; + line-height: 34px; + border-radius: 6px; } -.workbench-open-with-menu .ant-dropdown-menu-title-content { +.workbench-open-with-menu .ant-menu-title-content { + display: block; + flex: 1; min-width: 0; - overflow: hidden; + margin-inline-start: 0 !important; +} + +.workbench-open-with-menu .ant-menu-item-active { + background: #efefee !important; +} + +.workbench-open-with-menu .open-with-back { + display: flex; + align-items: center; + gap: 8px; + width: 100%; + height: 34px; + margin-bottom: 4px; + padding-inline: 10px; + border-bottom: 1px solid #eeeeec; + color: #37352f; + font-size: 13px; + cursor: pointer; +} + +.workbench-open-with-menu .open-with-back:hover { + background: #f4f4f2; +} + +.workbench-open-with-menu :is(.open-with-back, .ant-menu-item):focus-visible { + outline: 2px solid #0b6bcb; + outline-offset: -2px; } .ui-panel { diff --git a/src/renderer/src/components/fileRenders/index.css b/src/renderer/src/components/fileRenders/index.css index 2341ba07..100a4989 100644 --- a/src/renderer/src/components/fileRenders/index.css +++ b/src/renderer/src/components/fileRenders/index.css @@ -79,3 +79,22 @@ outline: 1px solid #91b6d6; outline-offset: 2px; } + +.text-file-review .monaco-editor .diff-hidden-lines .unchanged-lines-label { + padding: 0; + border: 0; + background: transparent; + color: #787774; + font: inherit; + font-size: 12px; + cursor: pointer; +} + +.text-file-review .monaco-editor .diff-hidden-lines .unchanged-lines-label:hover { + color: #37352f; +} + +.text-file-review .monaco-editor .diff-hidden-lines .unchanged-lines-label:focus-visible { + outline: 2px solid #0b6bcb; + outline-offset: 2px; +} diff --git a/src/renderer/src/components/workbench/FileOpenWithMenu.tsx b/src/renderer/src/components/workbench/FileOpenWithMenu.tsx new file mode 100644 index 00000000..7cc9881c --- /dev/null +++ b/src/renderer/src/components/workbench/FileOpenWithMenu.tsx @@ -0,0 +1,190 @@ +import { useEffect, useRef, useState, type ReactElement } from 'react' +import { Menu, Popover, type MenuProps } from 'antd' +import { + HiArrowLeft, + HiArrowTopRightOnSquare, + HiCheck, + HiChevronRight, + HiComputerDesktop +} from 'react-icons/hi2' +import type { OpenWithApplication } from '../../../../shared/types' +import { workbenchSelectors } from '../../testing/selectors' + +type Props = { + children: ReactElement + applications: OpenWithApplication[] + preferredApplicationId?: string + loading: boolean + error: string | null + onLoad: () => void + onOpen: (application: OpenWithApplication) => void + onSetPreferred: (id: string | null) => Promise + onReveal: () => void +} + +export default function FileOpenWithMenu({ + children, + applications, + preferredApplicationId, + loading, + error, + onLoad, + onOpen, + onSetPreferred, + onReveal +}: Props): ReactElement { + const [open, setOpen] = useState(false) + const [settings, setSettings] = useState(false) + const [saving, setSaving] = useState(false) + const popup = useRef(null) + useEffect(() => { + if (open) popup.current?.querySelector('[role="menu"]')?.focus() + }, [open, settings]) + const applicationItems: MenuProps['items'] = applications.map((application) => ({ + key: application.id, + disabled: saving, + icon: application.iconDataUrl ? ( + + ) : ( + + ), + label: ( + + {application.name} + {settings + ? preferredApplicationId === application.id && ( + + ) + : application.isDefault && ( + 默认 + )} + + ) + })) + const applicationGroup: MenuProps['items'] = [ + { + type: 'group', + className: 'open-with-applications', + children: applications.length + ? applicationItems + : [ + { + key: 'empty', + disabled: true, + label: loading ? '正在获取可用应用…' : error || '没有可用的应用' + } + ] + } + ] + const items: MenuProps['items'] = settings + ? [ + { + key: 'system', + disabled: saving, + icon: , + label: ( + + 使用系统设置 + {!preferredApplicationId && ( + + )} + + ) + }, + { type: 'divider' }, + ...applicationGroup + ] + : [ + ...applicationGroup, + { type: 'divider' }, + { + key: 'settings', + disabled: !applications.length, + label: ( + + 默认打开应用… + + + ) + }, + { key: 'reveal', label: '打开所在文件夹' } + ] + const choose: MenuProps['onClick'] = async ({ key }) => { + if (saving) return + if (settings) { + setSaving(true) + try { + if (await onSetPreferred(key === 'system' ? null : key)) setSettings(false) + } finally { + setSaving(false) + } + } else if (key === 'settings') setSettings(true) + else { + setOpen(false) + if (key === 'reveal') onReveal() + else { + const application = applications.find((item) => item.id === key) + if (application) onOpen(application) + } + } + } + return ( + { + if (visible) popup.current?.querySelector('[role="menu"]')?.focus() + }} + onOpenChange={(next) => { + setOpen(next) + if (next) { + setSettings(false) + if (!applications.length && !loading) onLoad() + } + }} + content={ +
{ + if (event.key === 'Escape') { + event.stopPropagation() + event.preventDefault() + if (settings) setSettings(false) + else setOpen(false) + } + }} + > + {settings && ( + + )} + +
+ } + > + {children} +
+ ) +} diff --git a/src/renderer/src/components/workbench/TextFileDiffViewer.tsx b/src/renderer/src/components/workbench/TextFileDiffViewer.tsx index 4e7a7afd..2c0582e2 100644 --- a/src/renderer/src/components/workbench/TextFileDiffViewer.tsx +++ b/src/renderer/src/components/workbench/TextFileDiffViewer.tsx @@ -1,11 +1,15 @@ import { useEffect, useLayoutEffect, useRef, useState } from 'react' -import { Button, Spin } from 'antd' +import { Button, Spin, Tooltip } from 'antd' +import { HiArrowDown, HiArrowUp } from 'react-icons/hi2' +import { LuFoldVertical, LuUnfoldVertical } from 'react-icons/lu' import type { AiFileChangeRecord } from '../../../../shared/types' import { useEditableTextFile } from '../fileRenders/useEditableTextFile' import { workbenchSelectors } from '../../testing/selectors' import { monaco } from './monacoReview' type Counts = { insertCount: number; deleteCount: number } +const reviewActionClass = + '!h-7 !w-7 !min-w-7 !rounded-[6px] !p-0 !text-[#91918E] hover:!bg-[#F4F4F2] hover:!text-[#37352F] focus-visible:!outline-2 focus-visible:!outline-[#0B6BCB] focus-visible:!outline-offset-[-2px]' export function TextFileDiffViewer({ record, onCountsChange @@ -194,30 +198,67 @@ export function TextFileDiffViewer({ ) return ( -
-
- 本次运行前 · 只读 - 当前文件 · 可编辑 -
-
- - - +
+
+ + 本次运行前 + +
+ + 当前文件 + +
+ +
+
{file.saveError && (
diff --git a/src/renderer/src/components/workbench/WorkbenchContentPanel.tsx b/src/renderer/src/components/workbench/WorkbenchContentPanel.tsx index b0e3636e..6b87965a 100644 --- a/src/renderer/src/components/workbench/WorkbenchContentPanel.tsx +++ b/src/renderer/src/components/workbench/WorkbenchContentPanel.tsx @@ -1,6 +1,6 @@ import FileIcon from '../FileIcon' import React, { useEffect, useMemo, useState } from 'react' -import { Button, Dropdown, Empty, Spin, Tooltip, message, type MenuProps } from 'antd' +import { Button, Empty, Spin, Tooltip, message } from 'antd' import { HiArrowLeft, HiArrowTopRightOnSquare, @@ -30,6 +30,7 @@ import type { } from './types' import { workbenchSelectors } from '../../testing/selectors' import ChangeCountBadges from './ChangeCountBadges' +import FileOpenWithMenu from './FileOpenWithMenu' import WorkspacePathLabel from './WorkspacePathLabel' const TextFileDiffViewer = React.lazy(() => import('./TextFileDiffViewer').then((m) => ({ default: m.TextFileDiffViewer })) @@ -285,7 +286,7 @@ export const WorkbenchDocumentPreviewPanelBase: React.FC => { + const setPreferred = async (applicationId: string | null): Promise => { try { const result = await window.api.setPreferredOpenApplication({ filePath, applicationId }) if (!result.success) throw new Error(result.error) setOpenWithApplications(result.applications) setPreferredApplicationId(result.preferredApplicationId) + const name = result.applications.find((item) => item.id === applicationId)?.name + message.success( + name + ? `已将 ${name} 设为 ${openWithExtension(filePath)} 的默认打开应用` + : '已恢复使用系统设置' + ) + return true } catch (reason) { message.error(String(reason)) + return false } } @@ -348,61 +357,6 @@ export const WorkbenchDocumentPreviewPanelBase: React.FC 0 - ? openWithApplications.map((application) => ({ - key: application.id, - icon: application.iconDataUrl ? ( - - ) : ( - - ), - label: ( - - {application.name} - - {application.isDefault && !preferredApplicationId ? ( - 默认 - ) : null} - - ) - })) - : [ - { - key: 'empty', - disabled: true, - label: openWithLoading ? '正在获取可用应用…' : openWithError || '没有可用的应用' - } - ] - const openWithItems: MenuProps['items'] = [ - ...(openWithApplicationItems || []), - { type: 'divider' }, - { - key: 'follow-system-default', - label: '跟随系统默认', - onClick: () => { - void setPreferred(null) - } - }, - { - key: 'reveal-containing-folder', - label: '打开所在文件夹' - } - ] - return (
) : null} - { - if (key === 'reveal-containing-folder') { - void onRevealPath(revealPath) - return - } - const application = openWithApplications.find((item) => item.id === key) - if (application) void openWithApplication(application) - } + { + void loadOpenWithApplications() }} - onOpenChange={(open) => { - if (open && openWithApplications.length === 0 && !openWithLoading) { - void loadOpenWithApplications() - } + onOpen={(application) => { + void openWithApplication(application) + }} + onSetPreferred={setPreferred} + onReveal={() => { + void onRevealPath(revealPath) }} >
-
+ {showReviewActions ? (