Skip to content

feat(xlsx-preview): add artifact spreadsheet preview#16712

Open
zhangjiadi225 wants to merge 34 commits into
mainfrom
zhangjiadi225/excel-render-discussion
Open

feat(xlsx-preview): add artifact spreadsheet preview#16712
zhangjiadi225 wants to merge 34 commits into
mainfrom
zhangjiadi225/excel-render-discussion

Conversation

@zhangjiadi225

@zhangjiadi225 zhangjiadi225 commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Before this PR:

.xlsx artifacts had no inline preview — OfficePreviewPanel only supported docx/pptx, and spreadsheets fell through to the unsupported-format state.

After this PR:

The artifact preview pane renders .xlsx workbooks read-only:

  • Parsing off the UI thread: a Web Worker (ExcelJS + JSZip) produces a structured-cloneable render model — cells, deduped styles (fonts/fills/borders/alignment with theme-color resolution), merges, hidden rows/cols, per-sheet default row height/col width (sheetFormatPr), floating images, and charts.
  • Formulas: cached results are used when present; uncached formulas are evaluated with fast-formula-parser (5s budget, memoized, cycle-safe); unevaluable ones show the formula source greyed out. Number formats are applied to displayed values.
  • Virtualized grid: two-axis virtual scrolling (@tanstack/react-virtual) with sticky row/column headers, an independent merge layer, and a floating layer for images and ECharts-rendered charts (bar/line/pie/area; placeholder for unsupported types). The grid pads blank rows/cols beyond the used range so the viewport is always filled, Excel-style.
  • Cell text display: cells show whole lines only (wrap cells clamp by line count instead of slicing glyphs mid-line); clicking a cell opens an absolutely positioned overlay with the full content that covers neighbours without pushing the layout.
  • Zoom as photographic scaling: layout is fixed at zoom=1 and the content layer scales via CSS transform, so charts, fonts, borders, and wrap line counts never reflow across zoom levels.
  • Bottom bar (Excel-like): sheet tabs on the left, selected-cell address/formula in the middle, zoom controls on the right.
  • Robustness: rewrites default-namespace drawing XML (openpyxl output) that crashes ExcelJS, reads row definitions from the load path so cell-less hidden rows keep height 0, and caps preview at 20 MB / 200k rows / 500 cols with warnings.

Fixes: N/A

Why we need it and why it was done in this way

Spreadsheets are a common artifact output (LLM-generated reports), and users previously had to open them externally. A custom worker parser + virtualized DOM grid keeps the main thread responsive, gives full control over fidelity, and avoids shipping a heavyweight embedded spreadsheet app for a read-only preview.

The following tradeoffs were made:

  • Read-only v1: no editing, no cell-range selection, no frozen-pane rendering (parsed and reserved in the model).
  • Fidelity approximations: pattern fills rendered as solid fg color, rich-text run styles flattened, unsupported chart types get a labelled placeholder; parse warnings are logged.
  • No auto-fit row heights: rows without stored heights render at the sheet default; the click-to-expand overlay covers content that Excel would auto-fit at open time.
  • Photographic zoom means canvas charts/bitmap images blur slightly above 100% (inherent to scaling a rendered image); DOM text and gridlines stay crisp.

The following alternatives were considered:

  • Embedding a spreadsheet library (Univer/Luckysheet/x-spreadsheet): far heavier bundles and editing-oriented APIs for a read-only preview.
  • SheetJS HTML conversion: no virtualization for large sheets, poor style/chart fidelity.
  • CSS-free layout-scaled zoom (initial implementation): caused ECharts re-layout and wrap-line changes on zoom; replaced by transform scaling.

Links to places where the discussion took place: internal review session (Conductor).

Breaking changes

None.

Special notes for your reviewer

  • The worker→UI contract lives in renderModel.ts; all types are structured-cloneable, image binaries transfer as ArrayBuffer.

  • ExcelJS quirks worked around (each documented at the call site): default-namespace wsDr drawings, worksheet.model.rows dropping cell-less row definitions (read from the load-path Row objects instead), sheetFormatPr defaults surfaced via worksheet.properties.

  • Coordinate systems: virtualizers work in scroll space (sizes × zoom); rendering, merge visibility, and the selection overlay work in zoom=1 content space inside the transform layer.

  • Tests: 218 cases across parser (values/styles/merges/sizing/formulas/number formats/theme colors/charts), layout pure functions, grid rendering (virtualization, merges, zoom, overlay, floating layer), and panel state machine.

  • New dependencies — this feature adds echarts, exceljs, fast-formula-parser, jszip, numfmt, declared under devDependencies (renderer libraries bundled by Vite, matching how react/mermaid/katex are declared here — not dependencies, which is for unbundled main-process runtime deps). Net bundle cost is smaller than the count suggests, and each covers a distinct gap ExcelJS leaves:

    • jszip is already in the tree (ExcelJS, docx, docx-preview, epub, @aiden0z/pptx-renderer all depend on it — pnpm why jszip → a single 3.10.1). Declaring it directly just avoids relying on a transitive dep; it adds zero bundle weight. Used to read raw chart XML (ExcelJS parses no charts) and to rewrite openpyxl default-namespace drawing parts before ExcelJS crashes on them.
    • echarts dedupes with @aiden0z/pptx-renderer's copy — both range on ^6, resolve to a single 6.1.0, and import the same modular echarts/* subpaths, so one copy lands in the bundle. Draws the parsed charts.
    • exceljs (structure + full cell styles), fast-formula-parser (evaluates uncached formulas — ExcelJS only exposes cached results), and numfmt (renders values against Excel number-format codes — ExcelJS returns the code string, not the display text) each cover a capability ExcelJS does not provide and have no in-tree substitute.
  • numfmt vs SheetJS SSF (investigated, kept numfmt) — I checked whether @e965/xlsx (already a dependency, used by exportExcel.ts) could replace numfmt via its SSF module. It is a functional drop-in: number/percent/date/date-time/boolean, error-string passthrough, and invalid-format fallback all match, verified against parser.numberFormat.test.ts. Rejected on bundle size — the parser runs in an isolated Web Worker bundle that cannot share the SheetJS the main renderer already pulls in via exportExcel.ts, and SSF is only reachable through the full, non-tree-shakeable SheetJS build (~952 KB min); the standalone xlsx.zahl build exports a string payload, not a callable API. That is ~5× numfmt's tree-shakeable ESM (~196 KB source). Keeping numfmt is the leaner choice for the shipped worker bundle.

Checklist

This checklist is not enforcing, but it's a reminder of items that could be relevant to every PR.
Approvers are expected to review this list.

  • Branch: This PR targets the correct branch — main for active development, v1 for v1 maintenance fixes
  • PR: The PR description is expressive enough and will help future contributors
  • Code: Write code that humans can understand and Keep it simple
  • Refactor: You have left the code cleaner than you found it (Boy Scout Rule)
  • Upgrade: Impact of this change on upgrade flows was considered and addressed if required
  • Documentation: A user-guide update was considered and is present (link) or not required. Check this only when the PR introduces or changes a user-facing feature or behavior.
  • Self-review: I have reviewed my own code (e.g., via /gh-pr-review, gh pr diff, or GitHub UI) before requesting review from others

Release note

Preview .xlsx spreadsheets directly in the artifact preview pane: multi-sheet tabs, cell styles and merged cells, floating images and charts, formula results, click-to-expand cell content, and photographic zoom.

@zhangjiadi225 zhangjiadi225 marked this pull request as ready for review July 6, 2026 03:48
@zhangjiadi225 zhangjiadi225 requested a review from a team July 6, 2026 03:48

@AtomsH4 AtomsH4 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This review was translated automatically.

The scope of this change is primarily adding read-only preview for .xlsx artifacts: OfficePreviewPanel integration with xlsx, adding worker for parsing/rendering model, virtualized grid, chart rendering, formula/number format handling, i18n, and tests. The overall implementation and test coverage is fairly complete, and CI has passed. I've left 2 accessibility/design-system warnings, and I suggest addressing them before approval.


Original Content

本次改动范围主要是新增 artifact 的 .xlsx 只读预览:OfficePreviewPanel 接入 xlsx,新增 worker 解析/render model、虚拟化网格、图表渲染、公式/数字格式处理、i18n 与测试。整体实现和测试覆盖较完整,CI 已通过;我留了 2 个 accessibility/design-system warning,建议处理后再 approve。

Comment thread src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/XlsxGrid.tsx Outdated
@eeee0717 eeee0717 self-requested a review July 6, 2026 06:47

@eeee0717 eeee0717 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This review was translated automatically.

Findings:

  • Important: useXlsxWorkbook only rejects the compressed .xlsx byte length before handing the buffer to JSZip.loadAsync and ExcelJS.Workbook.xlsx.load; there is no entry-count, per-entry, or total uncompressed-size preflight. A crafted workbook can stay under the 20 MB source cap while expanding into very large worksheet XML/media in the parser worker, tying up memory/CPU before the preview can fail cleanly. Please add DOCX/PPTX-style ZIP central-directory limits before JSZip.loadAsync/ExcelJS and cover oversized entry/total-uncompressed cases in tests. src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/useXlsxWorkbook.ts:50, src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/worker/parseWorkbook.ts:394
  • Important: Formula range evaluation materializes every cell in onRange with unbounded nested loops before the deadline is checked again. A tiny workbook can contain a range like SUM(A1:XFD1048576) and hang or exhaust the worker while building the array. Please cap supported range dimensions and check the deadline inside the loop, returning unevaluated without materializing huge ranges. src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/worker/formulaEvaluator.ts:128
  • Important: Chart cache parsing trusts ptCount and sparse pt idx values from chart XML when allocating/extending arrays. Malformed XML can declare a huge count or far-out index and force large allocations before the per-chart catch can recover. Please bound these values to finite non-negative integers within a reasonable chart/workbook limit, or skip the cache and fall back to the referenced cells with a warning. src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/worker/chartXmlParser.ts:298
  • Important: Excel percentStacked charts are collapsed to the same stacked boolean as ordinary stacked charts, and buildChartOption then renders raw values. That makes 100% stacked Excel charts show absolute totals rather than percentages, which materially misrepresents the workbook. Please preserve a distinct percent-stacked state and normalize per category, or mark it unsupported with a warning. src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/worker/chartXmlParser.ts:467, src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/charts/buildChartOption.ts:22
  • Important: formatCellValue parses any string as a date when the cell has a date-like numFmt, so a literal text cell such as "1" can render as a date instead of the text the workbook stored. Please restrict the string date path to the strict ISO strings produced for real Date values, or carry Date identity explicitly. src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/worker/numberFormat.ts:53
  • Important: The merged-cell overlay fixes the visual duplicate, but the base virtualized layer still exposes role="gridcell" for every covered cell while the merge overlay exposes another gridcell for the same master coordinate. Assistive tech can see empty covered cells plus a second semantic master cell. Please make the merged cell the single semantic gridcell, or hide covered placeholders from the accessibility tree, and add a regression test for duplicate merged-cell coordinates. src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/XlsxGrid.tsx:548, src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/XlsxGrid.tsx:587
  • Suggestion: SheetRenderModel.frozen is parsed and mocked but intentionally unused by the v1 grid. Since this is new render-model surface with zero real consumer demand today, please either wire frozen panes into XlsxGrid with tests or delete the field/parser output until the UI consumes it. src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/renderModel.ts:35

CI: GitHub checks are green for the reviewed head except skipped auxiliary jobs; local pnpm lint/test/format was not run per repository review rules.

After fixing, please request eeee0717 for follow-up review.


Original Content

Findings:

  • Important: useXlsxWorkbook only rejects the compressed .xlsx byte length before handing the buffer to JSZip.loadAsync and ExcelJS.Workbook.xlsx.load; there is no entry-count, per-entry, or total uncompressed-size preflight. A crafted workbook can stay under the 20 MB source cap while expanding into very large worksheet XML/media in the parser worker, tying up memory/CPU before the preview can fail cleanly. Please add DOCX/PPTX-style ZIP central-directory limits before JSZip.loadAsync/ExcelJS and cover oversized entry/total-uncompressed cases in tests. src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/useXlsxWorkbook.ts:50, src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/worker/parseWorkbook.ts:394
  • Important: Formula range evaluation materializes every cell in onRange with unbounded nested loops before the deadline is checked again. A tiny workbook can contain a range like SUM(A1:XFD1048576) and hang or exhaust the worker while building the array. Please cap supported range dimensions and check the deadline inside the loop, returning unevaluated without materializing huge ranges. src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/worker/formulaEvaluator.ts:128
  • Important: Chart cache parsing trusts ptCount and sparse pt idx values from chart XML when allocating/extending arrays. Malformed XML can declare a huge count or far-out index and force large allocations before the per-chart catch can recover. Please bound these values to finite non-negative integers within a reasonable chart/workbook limit, or skip the cache and fall back to the referenced cells with a warning. src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/worker/chartXmlParser.ts:298
  • Important: Excel percentStacked charts are collapsed to the same stacked boolean as ordinary stacked charts, and buildChartOption then renders raw values. That makes 100% stacked Excel charts show absolute totals rather than percentages, which materially misrepresents the workbook. Please preserve a distinct percent-stacked state and normalize per category, or mark it unsupported with a warning. src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/worker/chartXmlParser.ts:467, src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/charts/buildChartOption.ts:22
  • Important: formatCellValue parses any string as a date when the cell has a date-like numFmt, so a literal text cell such as "1" can render as a date instead of the text the workbook stored. Please restrict the string date path to the strict ISO strings produced for real Date values, or carry Date identity explicitly. src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/worker/numberFormat.ts:53
  • Important: The merged-cell overlay fixes the visual duplicate, but the base virtualized layer still exposes role="gridcell" for every covered cell while the merge overlay exposes another gridcell for the same master coordinate. Assistive tech can see empty covered cells plus a second semantic master cell. Please make the merged cell the single semantic gridcell, or hide covered placeholders from the accessibility tree, and add a regression test for duplicate merged-cell coordinates. src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/XlsxGrid.tsx:548, src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/XlsxGrid.tsx:587
  • Suggestion: SheetRenderModel.frozen is parsed and mocked but intentionally unused by the v1 grid. Since this is new render-model surface with zero real consumer demand today, please either wire frozen panes into XlsxGrid with tests or delete the field/parser output until the UI consumes it. src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/renderModel.ts:35

CI: GitHub checks are green for the reviewed head except skipped auxiliary jobs; local pnpm lint/test/format was not run per repository review rules.

修复后请 request eeee0717 再进行后续 review。

@zhangjiadi225

zhangjiadi225 commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

This comment was translated automatically.

@eeee0717 Thanks for the review, all 7 findings have been fixed and pushed (one commit per finding, each with unit tests):

  1. ZIP decompression preflight — Fixed (commit 03a6e12). Generalized the docx-specific docxZipPreflight to office/zipPreflight (error message format name parameterized, limits consistent with DOCX: 4000 entries / 32MB per-entry / 256MB total, Word side behavior unchanged). parseWorkbook now performs central directory preflight on raw bytes before JSZip.loadAsync/ExcelJS. Added three rejection test cases for oversized entry / oversized total / oversized entry count (manually constructed central directory bytes, can fake declared sizes).

  2. Formula range unbounded materialization — Fixed (commit e866c4e). onRange first rejects ranges exceeding 100k cells by area, then checks deadline every 1024 cells within the loop; both paths throw into existing catch, unified as unevaluated. Added test cases: SUM(A1:XFD1048576) returns quickly without reading any cells, mid-loop timeout interruption, large but bounded ranges still evaluate normally.

  3. Chart cache ptCount/idx trust issue — Fixed (commit ed3018f). ptCount and pt idx must be finite non-negative integers ≤10000; out-of-bounds values discard the cache, log a warning, and fall back to cell references (existing fallback path in readCategoriesOrValues). Added three test cases for huge ptCount, far-end sparse idx, and non-integer ptCount, all asserting fallback retrieves reference data.

  4. percentStacked stacking — Fixed (commit 590ed64). Changed ChartModel.stacked: boolean to stacking: 'stacked' | 'percentStacked'. buildChartOption normalizes percentStacked to percentage by category total, with value axis max 100 + % scale; maps to gap when category total is 0 or value is missing. Normal stacked behavior unchanged.

  5. String misjudged as date — Fixed (commit e9c3659). String date path now only accepts strict toISOString() format (YYYY-MM-DDTHH:mm:ss.sssZ, i.e., the raw form stored by parseWorkbook for Date); "1", "2026", "2026-01-15" etc. are all rendered as text. Added regression test cases.

  6. Merged cell duplicate gridcell — Fixed (commit 14dc6bf). Base virtual layer placeholder cells covered by merge (including master) have role="gridcell"/aria-* removed and aria-hidden added; merge layer becomes the sole semantic gridcell for each merged coordinate. Added regression test cases asserting master coordinate has only one gridcell and covered placeholders are hidden from AT.

  7. frozen field has no consumer — Deleted (commit 70ca1af). renderModel.frozen, parseWorkbook parsing, and mockModel test cases all removed together; will be added back when frozen panes are actually implemented.

Supplementary note: Local full pnpm test has an occasional failure in AgentComposer.test.tsx unrelated to this PR (reproduces on clean branch, passes when run alone, existing flake under concurrent load); office directory 16 files 261 tests all pass.


Original Content

@eeee0717 感谢 review,7 条发现已全部修复并 push(每条一个 commit,均附单测):

  1. ZIP 解压预检 — 已修复(commit 03a6e12)。把 docx 专用的 docxZipPreflight 泛化为 office/zipPreflight(错误文案格式名参数化,限额与 DOCX 一致:4000 entries / 32MB per-entry / 256MB total,Word 侧行为不变),parseWorkbookJSZip.loadAsync/ExcelJS 之前对原始字节做中央目录预检。新增超限 entry / 超限总量 / 超限条目数三个拒绝用例(手工构造中央目录字节,可伪造声明尺寸)。

  2. 公式 range 无界物化 — 已修复(commit e866c4e)。onRange 先按面积拒绝超过 100k 格的区间,循环内每 1024 格检查一次 deadline;两条路径均 throw 进现有 catch 统一归为 unevaluated。新增用例:SUM(A1:XFD1048576) 不读任何格子即快速返回、mid-loop 超时中断、大而有界区间仍正常求值。

  3. 图表缓存 ptCount/idx 信任问题 — 已修复(commit ed3018f)。ptCountpt idx 必须是 ≤10000 的有限非负整数,越界即弃用该缓存、记 warning 并回退到单元格引用(readCategoriesOrValues 已有的回退路径)。新增巨大 ptCount、远端稀疏 idx、非整数 ptCount 三个用例,均断言回退取到引用数据。

  4. percentStacked 折叠 — 已修复(commit 590ed64)。ChartModel.stacked: boolean 改为 stacking: 'stacked' | 'percentStacked',buildChartOption 对 percentStacked 按类目合计归一化为百分比,数值轴 max 100 + % 刻度;类目合计为 0 或值缺失时映射为 gap。普通 stacked 行为不变。

  5. 字符串误判日期 — 已修复(commit e9c3659)。字符串日期路径只接受 toISOString() 的严格形态(YYYY-MM-DDTHH:mm:ss.sssZ,即 parseWorkbook 对 Date 存的 raw 形态);"1""2026""2026-01-15" 等一律按文本渲染,新增回归用例。

  6. 合并单元格重复 gridcell — 已修复(commit 14dc6bf)。基础虚拟层被合并覆盖的占位格(含 master)移除 role="gridcell"/aria-* 并加 aria-hidden,合并层成为每个合并坐标唯一的语义 gridcell。新增回归用例断言 master 坐标只有一个 gridcell 且覆盖占位对 AT 隐藏。

  7. frozen 字段无消费者 — 已删除(commit 70ca1af)。renderModel.frozen、parseWorkbook 的解析、mockModel 用例三处一并移除,待冻结窗格真正落地时再加回。

补充说明:本地全量 pnpm testAgentComposer.test.tsx 有一个与本 PR 无关的偶发失败(干净分支同样复现、单独运行稳定通过,系并发负载下的既有 flake),office 目录 16 个文件 261 个测试全部通过。

@zhangjiadi225 zhangjiadi225 requested a review from eeee0717 July 6, 2026 09:19

@eeee0717 eeee0717 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed current head 167cc882e22432b40cd6e7d0935ee020d770e893 for the renewed review request. GitHub CI is passing (notify skipped), and I did not run local pnpm lint/test/format per repo review rules.

The seven issues from my previous review appear addressed by inspection: ZIP central-directory preflight now runs before JSZip/ExcelJS, formula ranges are capped/deadline-checked, chart cache counts/indices are bounded, percentStacked is preserved and normalized, string date parsing is strict, merged placeholders are hidden from the accessibility tree, and the unused frozen field was removed.

Blocking findings:

  1. src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/worker/parseWorkbook.ts:765 still lets chart reference fallback materialize unbounded ranges. When chart cache data is missing or rejected, chartXmlParser calls data.readRange(ref), and this implementation loops every row/col in the parsed A1 reference with no area cap. A tiny workbook can point a chart at Sheet1!$A$1:$XFD$1048576 and tie up or exhaust the worker despite the new cache ptCount guard and ZIP preflight. Please cap chart reference ranges before building the 2D array, similar to the formula MAX_RANGE_CELLS guard, and skip/mark oversized references with a warning.

  2. src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/worker/chartXmlParser.ts:193 feeds untrusted drawing anchor coordinates directly into layout.colX(point.col + 1) / layout.rowY(point.row + 1), whose implementations in parseWorkbook.ts:748 and :755 loop up to the supplied coordinate. A crafted chart anchor with a huge <xdr:col> or <xdr:row> can pin the parser worker before chart-level recovery. Please validate/clamp anchor coordinates to the preview limits or make out-of-range offset calculation O(1) and mark the chart/sheet truncated.

  3. src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/worker/parseWorkbook.ts:538 stores Date cells in rawValueTable as ISO strings, so uncached formulas that reference dates lose Excel serial-number semantics. For example, if A1 is a date, =A1+1 should evaluate to the next day, but the formula parser receives an ISO string and returns a value error/unevaluated result. The render model can keep ISO raw, but the formula evaluation context needs a serial value that respects the workbook date system.

Non-blocking follow-ups:

  • src/renderer/components/chat/panes/ArtifactPane.tsx:374 applies the generic 2 MB artifact cap before the office-preview branch, so artifact-pane .xlsx files between 2 MB and the new 20 MB xlsx limit never reach XlsxPreviewPanel or its office actions. Office previews should bypass the text cap like PDF does, or the advertised xlsx limit is not reachable from this UX.
  • src/renderer/i18n/locales/zh-tw.json:7802 adds primary Traditional Chinese locale strings as literal [to be translated]:... placeholders. Those are user-visible in zh-TW mode; please ship real zh-TW strings in locales/zh-tw.json rather than placeholder text.

@zhangjiadi225

Copy link
Copy Markdown
Collaborator Author

@eeee0717 Thanks for the second-round review. The three blocking findings are fixed (one commit each, all with unit tests); one non-blocking item is fixed and the other appears to be a false positive — details below.

Blocking

  1. Chart reference range unbounded materialization — Fixed (67d7b482e5). Extracted the chart-reference read into a pure readRangeFromValueTable; ranges above MAX_CHART_RANGE_CELLS (100k) by area now throw, and safeReadRange's existing catch treats them as missing data. Sheet1!$A$1:$XFD$1048576 no longer materializes. Added a guard test (oversized throws, bounded range still reads).

  2. Drawing anchor offset unbounded loop — Fixed (a2fd9a26cf). colX/rowY now use axisOffsetPx, which assumes the default size for all preceding tracks and corrects only the custom-sized ones, so cost is bounded by the sheet's custom-track count and is independent of the anchor coordinate. In-range anchors produce identical results. Added a test asserting a billion-scale coordinate returns instantly.

  3. Date serial semantics lost in formula context — Fixed (4598fdc7cf). The raw value table now stores a 1900-system Excel serial for dates instead of the ISO string, so =A1+1 on a date evaluates to the next day. The render model still keeps the ISO raw; both plain date cells and cached formula results that are dates are covered. Added a regression test.

Non-blocking

  • zh-TW placeholders — Fixed (44788cbbf1). The xlsx_preview keys now ship real Traditional Chinese (Taiwan terminology: 檔案 / 支援). i18n:check passes.

  • 2 MB artifact cap before the office branch — I believe this one is already handled and doesn't reproduce. oversizedForPreview already excludes office documents at ArtifactPane.tsx:232 (!isOfficeDocumentPreview), and that line is present at the reviewed head 167cc882e2. So a .xlsx between 2 MB and 20 MB has oversizedForPreview === false, skips the "too large" EmptyState, and reaches OfficePreviewPanel, which enforces the 20 MB limit internally via sourceSize. Could you double-check? If I'm misreading the path, happy to fix.

Tests: the ArtifactPreview/office directory (19 files / 271 tests) passes locally; per repo review rules I did not run pnpm lint/format.

Please request eeee0717 for follow-up review.

Original Content

@eeee0717 感谢第二轮 review。3 条 blocking 已修复(每条一个 commit,均附单测);1 条 non-blocking 已修,另 1 条经核查疑为误报,说明如下。

Blocking

  1. 图表引用 range 无界物化 — 已修复(67d7b482e5)。把图表引用读取抽为纯函数 readRangeFromValueTable,面积超过 MAX_CHART_RANGE_CELLS(100k)即 throw,safeReadRange 现有 catch 归为无数据。Sheet1!$A$1:$XFD$1048576 不再物化。新增守卫用例(超限 throw、有界区间仍正常读取)。

  2. 绘图锚点偏移无界循环 — 已修复(a2fd9a26cf)。colX/rowY 改用 axisOffsetPx:以默认尺寸为基线,只对自定义尺寸的轨道做修正,成本由自定义轨道数决定,与锚点坐标大小无关;有界锚点结果完全一致。新增用例断言十亿级坐标即时返回。

  3. 公式上下文丢失日期序列号语义 — 已修复(4598fdc7cf)。raw value table 对日期改存 1900 制 Excel serial 而非 ISO 字符串,=A1+1 现能求出次日;render model 的 raw 仍保留 ISO。普通日期格与缓存公式的日期结果两条路径都覆盖。新增回归用例。

Non-blocking

  • zh-TW 占位符 — 已修复(44788cbbf1)。xlsx_preview 各键补上真正的繁体中文(台湾用词:檔案 / 支援),i18n:check 通过。

  • 2 MB 上限挡在 office 分支之前 — 我认为此项已被处理、无法复现。oversizedForPreviewArtifactPane.tsx:232 已排除 office 文档(!isOfficeDocumentPreview),且该行在被 review 的 head 167cc882e2 即已存在。因此 2 MB~20 MB 的 .xlsxoversizedForPreview === false,会跳过 "too large" 的 EmptyState、进入 OfficePreviewPanel,由面板内部通过 sourceSize 判定 20 MB 上限。能否请你再核对一下?若是我看漏了路径,我很乐意修。

测试:本地 ArtifactPreview/office 目录(19 文件 / 271 用例)全部通过;按仓库 review 规则未跑 pnpm lint/format

修复后请 request eeee0717 再进行后续 review。

@eeee0717 eeee0717 self-requested a review July 6, 2026 10:27
zhangjiadi225 and others added 18 commits July 6, 2026 18:39
Signed-off-by: jd <59188306+zhangjiadi225@users.noreply.github.com>
Address review feedback on the spreadsheet preview:

- Grid now renders blank rows/cols beyond the used range: it fills the
  viewport at default cell sizes plus a scrollable buffer, re-measured
  on panel resize via ResizeObserver (Excel-like empty area).
- Drop the standalone status-bar row that duplicated the active sheet
  name; selected-cell address/formula now shows inline in the bottom
  bar only when a cell is selected.
- Drop the floating zoom overlay; zoom controls move to the right of
  the sheet tabs in a single bottom bar, and the zoom-reset button is
  removed (refresh already lives in the top toolbar).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: jd <59188306+zhangjiadi225@users.noreply.github.com>
… text mid-line

Row heights were dropping in two ways:

- sheetFormatPr defaultRowHeight/defaultColWidth were ignored — every
  non-overridden row/col rendered at the hardcoded 15pt/8.43ch global
  default. Now read per sheet (worksheet.properties) and threaded into
  the render model, image anchors, and chart layout.
- Row definitions were read via worksheet.model.rows, whose serializer
  drops cell-less rows (e.g. <row r="7" hidden="1"/>), un-hiding them.
  Now read straight from the Row objects on the load path.

Cell text display reworked to match normal spreadsheet behavior:

- Wrapped cells clip to whole lines (-webkit-line-clamp derived from
  the cell height) instead of slicing the overflow line in half, so a
  default-height row shows exactly one clean line.
- Clicking a cell opens an absolutely positioned overlay at the cell
  rect showing the full content (min cell size, max 480px wide); it
  covers neighbours without pushing the grid layout, replacing the old
  outline-only selection highlight.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: jd <59188306+zhangjiadi225@users.noreply.github.com>
Zooming previously multiplied layout sizes and font size, so elements
reflowed at each zoom level: the ECharts container resized (relaying
axes/legend or drifting relative to cells), wrapped text re-clamped to
a different line count, and fixed-px details (borders, padding, header
font) fell out of proportion.

Zoom is now a pure visual magnification, like enlarging a rendered
sheet from 400x400 to 800x800:

- All layout (axis tables, cell rects, fonts, floating objects, the
  selection overlay) is built at zoom=1.
- One content layer applies transform: scale(zoom) with top-left
  origin; sticky row/col header bars keep scroll-space boxes with a
  scaled inner layer so they stay aligned.
- Virtualizers keep working in scroll space via estimateSize * zoom,
  and the merge-layer viewport divides scroll coordinates back into
  content space.
- Chart containers no longer change layout size on zoom, so ECharts
  never re-lays-out; charts scale uniformly with the grid.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: jd <59188306+zhangjiadi225@users.noreply.github.com>
echarts, exceljs, fast-formula-parser, jszip and numfmt are imported only
from the renderer (XlsxPreviewPanel) and bundled by Vite, so they belong in
devDependencies alongside react/mermaid/katex — not dependencies, which is
for unbundled main-process runtime deps. Lockfile relocates the same
versions; no resolution changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: jd <59188306+zhangjiadi225@users.noreply.github.com>
Signed-off-by: jd <59188306+zhangjiadi225@users.noreply.github.com>
- Reset XlsxGrid internal selection when switching sheets by keying the grid
  on sheet name, removing the stale selection overlay that lingered on the
  newly-activated sheet while the status bar was empty
- Drop the write-only imageUrlsRef (URL revoke is handled by the effect cleanup)
- Cap per-column width expansion at MAX_COLS so whole-sheet <col> spans don't
  allocate thousands of unused entries cloned over postMessage
- Pass CellView position as primitive props so its React.memo can skip
  re-renders when a cell's position is unchanged

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: jd <59188306+zhangjiadi225@users.noreply.github.com>
Signed-off-by: jd <59188306+zhangjiadi225@users.noreply.github.com>
Signed-off-by: jd <59188306+zhangjiadi225@users.noreply.github.com>
Signed-off-by: jd <59188306+zhangjiadi225@users.noreply.github.com>
fast-formula-parser@1.0.19 registers MAX, MIN, MEDIAN, COUNTA, LARGE,
SMALL, MODE.SNGL, STDEV.S/.P and VAR.S/.P as empty stubs that return
undefined, so the library throws #NAME? "not implemented" and the
preview shows "公式无法求值" (e.g. =MAX(C2:C8) in a summary row).

Inject implementations via the FormulaParser `functions` option, which
merges after the built-ins and overrides the stubs. Aggregation follows
Excel semantics: text/blank cells are ignored, empty ranges yield 0 for
MAX/MIN, and out-of-range/degenerate inputs surface #NUM!/#N/A as
evaluated error results rather than "unevaluated".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: jd <59188306+zhangjiadi225@users.noreply.github.com>
Address PR review feedback on the xlsx artifact preview:

- Replace the hand-written `role="tab"` buttons with the shared
  `@cherrystudio/ui` Tabs primitives (line variant), gaining Radix
  roving focus and Arrow/Home/End keyboard navigation.
- Add keyboard cell selection to the grid: arrow keys move (clearing
  merged regions), Enter/Space select, Escape clears, and moves
  scrollToIndex the target so virtualized cells stay mounted.
- Complete grid semantics: role="grid" with aria-row/colcount on the
  focusable container, role="row" wrappers, and aria-row/colindex on
  gridcells.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: jd <59188306+zhangjiadi225@users.noreply.github.com>
SheetRenderModel.frozen was parsed and mocked but intentionally unused
by the grid; remove the speculative surface until a consumer exists.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: jd <59188306+zhangjiadi225@users.noreply.github.com>
The 20MB compressed-size cap cannot stop decompression bombs: a crafted
workbook can expand into multi-GB worksheet XML inside the parser worker.
Generalize the docx-only preflight into zipPreflight (format label in
messages, same limits) and run it on the raw bytes before JSZip/ExcelJS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: jd <59188306+zhangjiadi225@users.noreply.github.com>
onRange previously materialized every cell of a range with no area cap
and no deadline check inside the loop, so a tiny workbook containing
SUM(A1:XFD1048576) could hang or exhaust the worker. Reject ranges over
100k cells up front and honor the evaluation deadline mid-loop; both
paths throw into the existing catch that classifies the formula as
unevaluated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: jd <59188306+zhangjiadi225@users.noreply.github.com>
readCache allocated arrays straight from ptCount and extended them to
any sparse pt idx declared in chart XML, letting malformed input force
huge allocations before the per-chart catch could recover. Bound both
to finite non-negative integers within 10k points; out-of-range
declarations drop the cache with a warning and fall back to the
referenced cells.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: jd <59188306+zhangjiadi225@users.noreply.github.com>
…tages

percentStacked was collapsed into the same boolean as ordinary stacked
grouping, so 100% stacked Excel charts displayed absolute totals.
Preserve the grouping distinctly on ChartModel.stacking and have
buildChartOption normalize each category to percentages with a
0-100% value axis.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: jd <59188306+zhangjiadi225@users.noreply.github.com>
formatCellValue passed any string through new Date() when the cell had
a date-like numFmt, so literal text such as "1" rendered as a date.
Gate the string date path on the exact toISOString() shape that
parseWorkbook produces for real Date values.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: jd <59188306+zhangjiadi225@users.noreply.github.com>
zhangjiadi225 and others added 7 commits July 6, 2026 18:39
The base virtualized layer kept role=gridcell on every merge-covered
placeholder while the merge overlay exposed a second gridcell for the
master coordinate, so assistive tech saw empty covered cells plus a
duplicate master. Hide covered placeholders from the accessibility
tree; the merge overlay is now the only semantic gridcell for merged
coordinates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: jd <59188306+zhangjiadi225@users.noreply.github.com>
The barrel forwarded a bare default, which the barrel/named-only lint
rule now rejects on CI. Name the re-export and update the lazy loader
and its test mock accordingly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: jd <59188306+zhangjiadi225@users.noreply.github.com>
Signed-off-by: jd <59188306+zhangjiadi225@users.noreply.github.com>
Chart data references come from untrusted chart XML. When the cache is
missing or rejected, chartXmlParser falls back to readRange(ref), which
materialized every cell of the parsed A1 range with no area bound — a
reference like Sheet1!$A$1:$XFD$1048576 could tie up the worker before
the per-chart catch could recover.

Extract the range read into readRangeFromValueTable and reject references
above MAX_CHART_RANGE_CELLS by area, mirroring the formula evaluator's
guard. safeReadRange's existing catch turns the throw into missing data.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: jd <59188306+zhangjiadi225@users.noreply.github.com>
Chart drawing anchors carry untrusted <xdr:col>/<xdr:row> coordinates.
The layout colX/rowY closures summed track sizes by looping from 1 up to
the supplied coordinate, so a crafted anchor with a huge col/row could
pin the parser worker before the per-chart catch recovered.

Replace the loops with axisOffsetPx, which assumes the default size for
all preceding tracks and corrects only the custom-sized ones. Cost is
bounded by the sheet's actual custom-track count, independent of the
anchor coordinate; results are identical for in-range anchors.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: jd <59188306+zhangjiadi225@users.noreply.github.com>
Date cells stored their ISO string in the raw value table, so an
uncached formula referencing a date (e.g. =A1+1) received a string and
returned a value error instead of the next day. date1904 context is
available at read time and numfmt renders in the 1900 system, so a
1900-system serial is stored regardless of the workbook date system.

The render model still keeps the ISO raw on the cell; only the formula
evaluation table now holds the numeric serial, for both plain date cells
and cached formula results that are dates.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: jd <59188306+zhangjiadi225@users.noreply.github.com>
The xlsx_preview keys shipped as literal "[to be translated]" English
placeholders in the primary Traditional Chinese locale, so they were
user-visible in zh-TW mode. Replace them with real Taiwan-terminology
translations (檔案 / 支援).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: jd <59188306+zhangjiadi225@users.noreply.github.com>
@zhangjiadi225 zhangjiadi225 force-pushed the zhangjiadi225/excel-render-discussion branch from 44788cb to 450edcf Compare July 6, 2026 10:44
@zhangjiadi225 zhangjiadi225 requested a review from AtomsH4 July 6, 2026 13:00

@eeee0717 eeee0717 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This review was translated automatically.

Findings:

  • Important: src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/worker/parseWorkbook.ts:664 still computes floating image anchors with local colX / rowY helpers that loop up to the untrusted drawing coordinate. The chart-anchor path was moved to axisOffsetPx, but embedded images still call colX(tl.nativeCol + 1), rowY(tl.nativeRow + 1), and the same for br, so a small workbook with a tiny image and huge <xdr:col> / <xdr:row> can still pin the parser worker before the preview reaches a clean truncated/error state. Please reuse the bounded axisOffsetPx approach for image anchors too, or clamp/skip hostile image anchors before offset calculation, and add the corresponding hostile-coordinate regression test.

CI: GitHub checks are green for the reviewed head (changes, catalog-hand-edit-check, changeset-check, basic-checks, general-test, and render-test passed; notify skipped). Local pnpm lint/test/format was not run per repository review rules.

After fixing, please request eeee0717 for further review.


Original Content

Findings:

  • Important: src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/worker/parseWorkbook.ts:664 still computes floating image anchors with local colX / rowY helpers that loop up to the untrusted drawing coordinate. The chart-anchor path was moved to axisOffsetPx, but embedded images still call colX(tl.nativeCol + 1), rowY(tl.nativeRow + 1), and the same for br, so a small workbook with a tiny image and huge <xdr:col> / <xdr:row> can still pin the parser worker before the preview reaches a clean truncated/error state. Please reuse the bounded axisOffsetPx approach for image anchors too, or clamp/skip hostile image anchors before offset calculation, and add the corresponding hostile-coordinate regression test.

CI: GitHub checks are green for the reviewed head (changes, catalog-hand-edit-check, changeset-check, basic-checks, general-test, and render-test passed; notify skipped). Local pnpm lint/test/format was not run per repository review rules.

修复后请 eeee0717 再进行后续 review。

@AtomsH4 AtomsH4 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This review was translated automatically.

Found 1 blocker and 1 warning. The main risks are all in the new xlsx parsing/worker path: floating image anchor can still use untrusted large coordinates to cause long worker occupation; when switching files, the old parsing will also not be terminated. CI has checked and passed, but it is recommended to merge after fixing the blocker.


Original Content

发现 1 个 blocker 和 1 个 warning。主要风险都在新增 xlsx 解析/worker 路径:floating image anchor 仍可用不可信大坐标造成 worker 长时间占用;切换文件时旧解析也不会被终止。CI 已检查为通过,但建议修完 blocker 后再合并。


// Floating images.
const floatingImages: FloatingObjectModel[] = []
const colX = (col: number): number => {

@AtomsH4 AtomsH4 Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This review comment was translated automatically.

[A5][Blocker] The floating image path reimplemented colX/rowY here, internally looping from 1 to the passed coordinate; however, tl.nativeCol/nativeRow and br.nativeCol/nativeRow come from the xlsx drawing XML, and like the previously fixed chart anchor, are untrusted input. A very small workbook with image anchor written as huge <xdr:col>/<xdr:row> values will cause prolonged parser worker occupation at colX(tl.nativeCol + 1) or rowY(...); the ZIP pre-check, row/column limits, and stale response discard mechanisms don't take effect in time. It's recommended that the image path also reuse axisOffsetPx(col, colWidthsPx, defaultColWidthPx) / axisOffsetPx(row, rowHeightsPx, defaultRowHeightPx), and add a floating image large coordinate regression test.


Original Content

[A5][Blocker] floating image 路径这里重新实现了 colX/rowY,内部从 1 循环到传入坐标;但 tl.nativeCol/nativeRowbr.nativeCol/nativeRow 来自 xlsx drawing XML,和前面已修的 chart anchor 一样是不可信输入。一个很小的 workbook 只要把图片 anchor 写成超大 <xdr:col>/<xdr:row>,就会在 colX(tl.nativeCol + 1)rowY(...) 处长时间占用 parser worker;ZIP 预检、行列上限和 stale response discard 都来不及生效。建议图片路径也复用 axisOffsetPx(col, colWidthsPx, defaultColWidthPx) / axisOffsetPx(row, rowHeightsPx, defaultRowHeightPx),并补一个 floating image 大坐标回归测试。

worker.postMessage(request, [bytes])
})()

return () => {

@AtomsH4 AtomsH4 Jul 6, 2026

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This review comment was translated automatically.

[A5][Warning] When request is superseded by filePath/refreshKey/sourceSize, cleanup only sets cancelled = true, and the worker doesn't terminate until unmount. parseWorkbook may run for a while on complex but valid xlsx files; when the user switches to a new file, the new request goes to the same busy worker and gets queued, the old parsing continues occupying CPU, and if the old parsing crashes, it may set the new request to error through the current onerror (worker errors have no id for deduplication). Suggest using an independent worker for each parsing request, or terminating the current worker and creating a new one when a new request starts/cleanup; requestId can continue as a stale response fallback, while also updating the test expectations for the current "reusing worker" behavior.


Original Content

[A5][Warning] request 被 filePath/refreshKey/sourceSize supersede 时,cleanup 只设置 cancelled = true,worker 直到 unmount 才 terminate。parseWorkbook 对复杂但合法的 xlsx 可能运行较久;用户切换到新文件时,新 request 会发到同一个 busy worker 并排队,旧解析继续占用 CPU,旧解析崩溃还可能通过当前 onerror 把新 request 置为 error(worker error 没有 id 可去重)。建议每个解析请求使用独立 worker,或在新请求开始/cleanup 时 terminate 当前 worker 并创建新 worker;requestId 可继续作为 stale response 兜底,同时更新当前“reusing worker”的测试预期。

zhangjiadi225 and others added 2 commits July 6, 2026 22:08
Floating-image anchors came from untrusted drawing XML but computed
their pixel offset with local colX/rowY helpers that looped up to the
anchor coordinate. A tiny image with a billion-scale <xdr:col>/<xdr:row>
could pin the parser worker before the preview reached a clean truncated
state. Route image anchors through the bounded axisOffsetPx (already used
for chart anchors), whose cost depends only on the sheet's custom-track
count, not the anchor magnitude.

Add a hostile-coordinate regression test that injects a 1e9 anchor into
the stored drawing XML and asserts it resolves near-instantly (tight
timeout) with the sheet clamped and flagged truncated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: jd <59188306+zhangjiadi225@users.noreply.github.com>
The hook reused a single lazily-created worker and, on supersede
(filePath/refreshKey/sourceSize change), only set cancelled=true —
terminating the worker just on unmount. A slow but valid parse could
then pin CPU while the next file queued behind it on the same worker,
and a crash from the old parse (whose ErrorEvent carries no request id)
could flip the new request to error via the rebound onerror.

Each request now owns a dedicated worker that the effect cleanup
terminates on supersede/unmount, so switching files frees the old
parse's CPU immediately and isolates its onerror. requestId stays as a
stale-response fallback. Update the worker-reuse tests and add a
regression test for superseded-worker crash isolation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: jd <59188306+zhangjiadi225@users.noreply.github.com>
@zhangjiadi225

zhangjiadi225 commented Jul 6, 2026

Copy link
Copy Markdown
Collaborator Author

This comment was translated automatically.

@eeee0717 @AtomsH4 Thanks for the two rounds of review. This round includes 2 [A5] fixes that have been pushed (one commit each, both with tests):

  1. [A5][Blocker] Image anchor unbounded materialization parseWorkbook.ts:664 — Fixed (102134f1b5). Changed floating image's colX/rowY to use bounded axisOffsetPx(col/row, sizes, defaultSize) (same approach as the already-fixed chart anchor): using default size as baseline, only correcting轨道 for custom sizes, cost only related to the number of custom tracks in the sheet, not to anchor coordinate magnitude; bounded anchor results are identical. Added regression test for large coordinates — injecting billion-level <xdr:col>/<xdr:row> into stored drawing XML, asserting immediate return (timeout guard).

  2. [A5][Warning] Worker queuing/crosstalk reuse useXlsxWorkbook.ts — Fixed (bf838ec6c1). Changed to independent worker per parsing request: when request is superseded by filePath/refreshKey/sourceSize or component unmounts, terminate current worker in effect cleanup and nullify ref. When switching files, immediately release CPU occupied by old parsing; new file no longer queued behind busy worker; old worker's onerror without id won't set new request to error since worker is terminated and closure requestId doesn't match. requestId kept as stale response fallback. Updated original "worker reuse" test, added new superseded-worker crash isolation regression case.

Regarding the first round's [C7] two a11y warnings: verified already handled in earlier commits, so not covered in this review —

  • Hand-written button role="tab" → Changed to use @cherrystudio/ui's Tabs/TabsList/TabsTrigger (95bc8767e8);
  • gridcell missing keyboard selection → XlsxGrid added handleKeyDown (arrow keys / Enter / Space / Escape, merge-aware) + role="grid"/row/gridcell" and aria-* semantics, including keyboard-selection test; merged cells single semantic gridcell in 529315bd16.

Tests: All passed in ArtifactPreview/office directory (247 tests). Per repo review rules, didn't run pnpm lint/format locally, using CI as standard.

After fixing, please @eeee0717 @AtomsH4 proceed with follow-up review, thank you.


Original Content

@eeee0717 @AtomsH4 感谢两轮 review。本轮 2 条 [A5] 已修复并推送(各一个 commit,均附单测):

  1. [A5][Blocker] 图片锚点无界物化 parseWorkbook.ts:664 — 已修复(102134f1b5)。floating image 的 colX/rowY 改用有界的 axisOffsetPx(col/row, sizes, defaultSize)(与已修的 chart anchor 同一方案):以默认尺寸为基线,只对自定义尺寸的轨道做修正,成本只与工作表自定义轨道数相关、与锚点坐标大小无关;有界锚点结果完全一致。新增大坐标回归测试——向存储的 drawing XML 注入十亿级 <xdr:col>/<xdr:row>,断言即时返回(超时守卫)。

  2. [A5][Warning] 复用 worker 的排队/串扰 useXlsxWorkbook.ts — 已修复(bf838ec6c1)。改为每个解析请求独立 worker:在 request 被 filePath/refreshKey/sourceSize supersede 或组件卸载时,于 effect cleanup 中 terminate() 当前 worker 并置空 ref。切文件时立即释放旧解析占用的 CPU、新文件不再排队到 busy worker 后面;旧 worker 无 id 的 onerror 也因 worker 已终止且闭包 requestId 不匹配而不会把新请求置为 error。requestId 保留作 stale response 兜底。已更新原"复用 worker"测试,并新增 superseded-worker 崩溃隔离回归用例。

关于首轮的 [C7] 两条 a11y warning:经核对已在早前提交处理,故本轮 review 未再涉及——

  • 手写 button role="tab" → 已改用 @cherrystudio/uiTabs/TabsList/TabsTrigger(95bc8767e8);
  • gridcell 缺键盘选择 → XlsxGrid 已补 handleKeyDown(方向键 / Enter / Space / Escape,合并单元格感知)+ role="grid"/row/gridcell"aria-* 语义,含 keyboard-selection 单测;合并单元格单一语义 gridcell 见 529315bd16

测试:ArtifactPreview/office 目录全部通过(247 tests)。按仓库 review 规则未在本地跑 pnpm lint/format,以 CI 为准。

修复后请 @eeee0717 @AtomsH4 再进行后续 review,谢谢。

@eeee0717 eeee0717 self-requested a review July 7, 2026 02:23

@AtomsH4 AtomsH4 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This review was translated automatically.

LGTM. The previous round of xlsx floating image anchor blocker has been fixed to reuse bounded axisOffsetPx, and added hostile image anchor regression test; worker supersede cleanup also changed to terminate the current worker, to avoid old parsing continuing to occupy CPU or affect new requests through id-less onerror. render-test passed; general-test currently fails on unrelated file watcher timeout (src/main/services/file/tests/watcher.test.ts), which does not belong to this PR's scope of changes.


Original Content

LGTM。上一轮的 xlsx floating image anchor blocker 已修复为复用 bounded axisOffsetPx,并补了 hostile image anchor 回归测试;worker supersede cleanup 也改为终止当前 worker,避免旧解析继续占用 CPU 或通过 id-less onerror 影响新请求。render-test 通过;general-test 当前失败在 unrelated file watcher timeout(src/main/services/file/tests/watcher.test.ts),不属于本 PR 改动范围。

Signed-off-by: jd <59188306+zhangjiadi225@users.noreply.github.com>

@AtomsH4 AtomsH4 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This review was translated automatically.

LGTM. The old issues with xlsx floating image anchor and worker supersede cleanup have been fixed and have regression coverage; the latest commit only stabilizes the file watcher test wait order, and the current head's basic/general/render and other key CI all passed.


Original Content

LGTM。xlsx floating image anchor 与 worker supersede cleanup 的旧问题已修复并有回归覆盖;最新提交仅稳定 file watcher 测试等待顺序,当前 head 的 basic/general/render 等关键 CI 均通过。

This reverts commit ae89cbe.

Signed-off-by: jd <59188306+zhangjiadi225@users.noreply.github.com>
…ender-discussion

Signed-off-by: jd <59188306+zhangjiadi225@users.noreply.github.com>

@eeee0717 eeee0717 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This review was translated automatically.

Reviewed current head 730890428ae08bcd12d0a7bbae7de86fe9f39390 for the renewed review request. The earlier ZIP preflight, unbounded range/anchor, worker supersede, merged-cell, and zh-TW placeholder issues appear fixed, and GitHub CI is green. I still found current-head issues:

Findings:

  • Important: src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/worker/parseWorkbook.ts:540 / :601 still store date cells into the formula raw value table as 1900-system serials even when workbook.properties.date1904 is true, while numberFormat.ts:36 ignores date1904. In 1904 workbooks, Excel formula semantics use 1904 serials, so formulas/comparisons like =A1, =A1=44575, or downstream chart values derived from formula output are off by 1462 serial units. Date-formatted =A1+1 can look correct only because the wrong 1900 serial is later formatted with a 1900 formatter. Please evaluate formulas with the workbook date system and convert only at display time, with a 1904 regression test.
  • Important: src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/charts/buildChartOption.ts:17 normalizes percentStacked by the signed sum of all series. All-negative categories become positive percentages, and mixed positive/negative categories can exceed 100 then clip against the fixed axis max. Please preserve sign and normalize positive/negative stacks separately, with an axis min when negative percentages exist.
  • Important: src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/worker/parseWorkbook.ts:197 strips outer quotes from chart reference sheet names but does not unescape doubled apostrophes. A valid reference like 'Bob''s Data'!$A$1 looks up Bob''s Data!1:1 instead of Bob's Data!1:1, so no-cache chart fallback loses data for sheets with apostrophes.
  • Warning: src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/XlsxGrid.tsx:620 keys floating images only by imageId, but parseWorkbook intentionally reuses one render image id for multiple anchors of the same workbook image. Repeated placements of the same image produce duplicate React keys and can reconcile the wrong DOM node; include a per-anchor identifier such as rect coordinates or add a unique floating-object id.
  • Warning: src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/XlsxPreviewPanel.tsx:146 renders state.message directly in the user-facing error state. The worker/parse path returns hardcoded English/internal messages from ZIP preflight, parse, and file-read failures, bypassing the repo i18n rule in non-English locales. Please map parse failures to i18n keys or a generic translated preview error and log the technical detail.

CI: GitHub checks are green for this head (changes, catalog-hand-edit-check, changeset-check, basic-checks, general-test, render-test, and translate passed; notify skipped). git diff --check passed. Local pnpm lint/test/format was not run per repository review rules.

After fixing, please request eeee0717 for further review.


Original Content

Reviewed current head 730890428ae08bcd12d0a7bbae7de86fe9f39390 for the renewed review request. The earlier ZIP preflight, unbounded range/anchor, worker supersede, merged-cell, and zh-TW placeholder issues appear fixed, and GitHub CI is green. I still found current-head issues:

Findings:

  • Important: src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/worker/parseWorkbook.ts:540 / :601 still store date cells into the formula raw value table as 1900-system serials even when workbook.properties.date1904 is true, while numberFormat.ts:36 ignores date1904. In 1904 workbooks, Excel formula semantics use 1904 serials, so formulas/comparisons like =A1, =A1=44575, or downstream chart values derived from formula output are off by 1462 serial units. Date-formatted =A1+1 can look correct only because the wrong 1900 serial is later formatted with a 1900 formatter. Please evaluate formulas with the workbook date system and convert only at display time, with a 1904 regression test.
  • Important: src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/charts/buildChartOption.ts:17 normalizes percentStacked by the signed sum of all series. All-negative categories become positive percentages, and mixed positive/negative categories can exceed 100 then clip against the fixed axis max. Please preserve sign and normalize positive/negative stacks separately, with an axis min when negative percentages exist.
  • Important: src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/worker/parseWorkbook.ts:197 strips outer quotes from chart reference sheet names but does not unescape doubled apostrophes. A valid reference like 'Bob''s Data'!$A$1 looks up Bob''s Data!1:1 instead of Bob's Data!1:1, so no-cache chart fallback loses data for sheets with apostrophes.
  • Warning: src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/XlsxGrid.tsx:620 keys floating images only by imageId, but parseWorkbook intentionally reuses one render image id for multiple anchors of the same workbook image. Repeated placements of the same image produce duplicate React keys and can reconcile the wrong DOM node; include a per-anchor identifier such as rect coordinates or add a unique floating-object id.
  • Warning: src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/XlsxPreviewPanel.tsx:146 renders state.message directly in the user-facing error state. The worker/parse path returns hardcoded English/internal messages from ZIP preflight, parse, and file-read failures, bypassing the repo i18n rule in non-English locales. Please map parse failures to i18n keys or a generic translated preview error and log the technical detail.

CI: GitHub checks are green for this head (changes, catalog-hand-edit-check, changeset-check, basic-checks, general-test, render-test, and translate passed; notify skipped). git diff --check passed. Local pnpm lint/test/format was not run per repository review rules.

修复后请 eeee0717 再进行后续 review。

… error i18n

- Unescape doubled apostrophes in quoted chart-reference sheet names so
  `'Bob''s Data'!$A$1` resolves to the `Bob's Data` sheet in the no-cache
  chart fallback.
- Disambiguate floating-image React keys by anchor index; one workbook image
  is intentionally reused across anchors, so keying by imageId alone collided.
- Show a translated generic description in the xlsx error state instead of the
  raw worker message, and log the technical detail at the failure site.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: jd <59188306+zhangjiadi225@users.noreply.github.com>
@zhangjiadi225

zhangjiadi225 commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

This comment was translated automatically.

Thanks for the review. The following changes are made for the current head (commit b70bd8247c):

Fixed (all with regression tests):

  • 缺一些常用功能 #3 Chart reference worksheet name double quotes: parseA1Range removes outer quotes and then adds ''' unescaping, 'Bob''s Data'!$A$1 can now correctly parse to Bob's Data (no-cache chart fallback path). Also confirmed that parseCharts another path gets the worksheet.name already cleaned by ExcelJS, not affected.
  • 非常棒,简洁,配置方便! #4 Floating image React key duplicate: Same workbook images at multiple anchors will reuse the same imageId, key changed from img:${imageId} to img:${imageId}:${index}, eliminating duplicate keys. Added test case (same image two anchors → render two imgs without duplicate-key warning).
  • 一些建议 #5 Error message not i18n: Error state description changed to use t('xlsx_preview.error.description') (new key), original technical info is recorded via logger.error at the failure point in useXlsxWorkbook, no longer directly output to UI.

Not handled for now (with reasons):

  • SyntaxError: Unexpected token 'E', "Error: abo"... is not valid JSON #1 1904 date system / formula raw value table: To produce an error display, all of the following need to be met: (a) 1904 date system workbook (only old Mac Excel), (b) formula has no cached result and needs re-evaluation, (c) formula mixes date cell references with bare date serial number literals (e.g., =A1=44575); however, the date formatting path itself uses the 1900 system for correct rendering. The top comment in numberFormat.ts explains this is intentional design—ExcelJS reads using date1904 to convert the source serial number to the correct absolute date, then stores it back as a 1900 serial number for numfmt rendering. Implementing the date system throughout the entire formula engine would be excessive engineering with minimal benefit, so the current state is preserved.
  • 配置openai的时候,不支持配置第三方的url #2 percentStacked negative value normalization: 100% stacked charts semantically are used for non-negative component data, negative value scenarios are very rare; the existing implementation is correct for the common all-positive scenario. Precisely matching Excel's positive/negative sub-stacking with axis min modifications would have high cost and low benefit, so not handled for now.

If needed, I can add a comment for #1/#2 each noting the known limitation. After the fix, please @eeee0717 for follow-up review, thanks.


Original Content

感谢 review。本次针对当前 head 处理如下(commit b70bd8247c):

已修复(均附回归测试):

  • 缺一些常用功能 #3 图表引用工作表名双撇号:parseA1Range 去除外层引号后补充 ''' 反转义,'Bob''s Data'!$A$1 现能正确解析到 Bob's Data(no-cache 图表回退路径)。另确认 parseCharts 的另一条路径拿的是已由 ExcelJS 清洗过的 worksheet.name,不受影响。
  • 非常棒,简洁,配置方便! #4 浮动图片 React key 重复:同一 workbook 图片在多锚点会复用同一 imageId,key 由 img:${imageId} 改为 img:${imageId}:${index},消除重复 key。新增用例(同图两锚点 → 渲染两个 img 且无 duplicate-key 警告)。
  • 一些建议 #5 错误信息未 i18n:错误态描述改用 t('xlsx_preview.error.description')(新增 key),原始技术信息在 useXlsxWorkbook 失败点通过 logger.error 记录,不再直出到 UI。

暂不处理(附理由):

  • SyntaxError: Unexpected token 'E', "Error: abo"... is not valid JSON #1 1904 日期系统 / 公式原始值表:要产生错误显示需同时满足 (a) 1904 日期系统工作簿(仅老 Mac Excel)、(b) 公式无缓存结果需进入二次求值、(c) 公式将日期单元格引用与裸日期序列号字面量混用(如 =A1=44575);而日期格式化路径本身用 1900 系统渲染是正确的。numberFormat.ts 顶部注释已说明这是刻意设计——ExcelJS 读取时已用 date1904 把源序列号转成正确的绝对日期,存回 1900 序列号供 numfmt 渲染。为此把日期系统贯穿整个公式引擎属于过度工程、收益极低,故保留现状。
  • 配置openai的时候,不支持配置第三方的url #2 percentStacked 负值归一化:100% 堆叠图语义上用于非负构成数据,负值场景非常罕见;现有实现对全正的常见场景是正确的。为负值精确匹配 Excel 的正负分栈 + 轴 min 改动成本较高、收益极低,暂不处理。

如需,我可以为 #1/#2 各加一行注释标注已知限制。修复后请 @eeee0717 再进行后续 review,谢谢。

@zhangjiadi225 zhangjiadi225 requested a review from eeee0717 July 7, 2026 04:00

@eeee0717 eeee0717 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the renewed request for current head 7308904. This is the same head I reviewed in my latest changes-requested review, so I am not duplicating the same findings here.

The prior current-head findings still need either fixes or an explicit maintainer decision before I can approve. Core GitHub CI checks for this head are green (changes, catalog-hand-edit-check, changeset-check, basic-checks, general-test, render-test; notify skipped). Local pnpm lint/test/format was not run per repository review rules.

@zhangjiadi225 zhangjiadi225 requested a review from eeee0717 July 7, 2026 05:12

@eeee0717 eeee0717 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This review was translated automatically.

Reviewed current head cbe661cbfee4a34cb77476fc7ead172c79c33bcd for the renewed request. The prior ZIP/range/anchor, sheet-name apostrophe, duplicate floating-image key, raw error i18n, zh-TW placeholder, worker supersede, and merged-cell accessibility issues appear fixed. I still found current-head issues:

Findings:

  • Important: src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/worker/parseWorkbook.ts:543 / :605 still store formula-context date values as 1900-system serials even when workbook.properties.date1904 is true, and numberFormat.ts:36 discards the date1904 flag. Date-formatted =A1+1 can look correct because it is later formatted through the same 1900 convention, but uncached formula numeric results, comparisons to workbook serial literals, and chart fallback values can be off by 1462 days in 1904 workbooks. Please evaluate formulas in the workbook date system and convert only at display time, with a 1904 uncached-formula regression test.
  • Important: src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/charts/buildChartOption.ts:17 still normalizes percentStacked by a single signed category total. All-negative categories become positive percentages, and mixed positive/negative categories can exceed the fixed max: 100 axis and clip. Please preserve sign by normalizing positive and negative stacks separately and set an axis minimum when negative percentages exist, or explicitly downgrade unsupported negative percent-stacked charts with coverage.
  • Important: src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/worker/chartXmlParser.ts:516 / :518 parse every untrusted chart anchor with no count cap, and XlsxGrid.tsx:636 renders every parsed chart. A small drawing XML can repeat thousands of anchors pointing at the same chart relationship; ZIP size/range/coordinate caps still pass, but the worker repeatedly reads/parses the chart part and the renderer can create thousands of chart hosts/ECharts instances. Please add a per-sheet/workbook floating-object cap, memo parsed chart parts by path, and skip excess objects with a warning. The same object cap should cover floating images from parseWorkbook.ts:670 and XlsxGrid.tsx:615.

CI: GitHub checks are green for this head (changes, catalog-hand-edit-check, changeset-check, basic-checks, general-test, and render-test passed; notify skipped). Local pnpm lint/test/format was not run per repository review rules.

After fixing, please request eeee0717 for further review.


Original Content

Reviewed current head cbe661cbfee4a34cb77476fc7ead172c79c33bcd for the renewed request. The prior ZIP/range/anchor, sheet-name apostrophe, duplicate floating-image key, raw error i18n, zh-TW placeholder, worker supersede, and merged-cell accessibility issues appear fixed. I still found current-head issues:

Findings:

  • Important: src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/worker/parseWorkbook.ts:543 / :605 still store formula-context date values as 1900-system serials even when workbook.properties.date1904 is true, and numberFormat.ts:36 discards the date1904 flag. Date-formatted =A1+1 can look correct because it is later formatted through the same 1900 convention, but uncached formula numeric results, comparisons to workbook serial literals, and chart fallback values can be off by 1462 days in 1904 workbooks. Please evaluate formulas in the workbook date system and convert only at display time, with a 1904 uncached-formula regression test.
  • Important: src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/charts/buildChartOption.ts:17 still normalizes percentStacked by a single signed category total. All-negative categories become positive percentages, and mixed positive/negative categories can exceed the fixed max: 100 axis and clip. Please preserve sign by normalizing positive and negative stacks separately and set an axis minimum when negative percentages exist, or explicitly downgrade unsupported negative percent-stacked charts with coverage.
  • Important: src/renderer/components/ArtifactPreview/office/XlsxPreviewPanel/worker/chartXmlParser.ts:516 / :518 parse every untrusted chart anchor with no count cap, and XlsxGrid.tsx:636 renders every parsed chart. A small drawing XML can repeat thousands of anchors pointing at the same chart relationship; ZIP size/range/coordinate caps still pass, but the worker repeatedly reads/parses the chart part and the renderer can create thousands of chart hosts/ECharts instances. Please add a per-sheet/workbook floating-object cap, memo parsed chart parts by path, and skip excess objects with a warning. The same object cap should cover floating images from parseWorkbook.ts:670 and XlsxGrid.tsx:615.

CI: GitHub checks are green for this head (changes, catalog-hand-edit-check, changeset-check, basic-checks, general-test, and render-test passed; notify skipped). Local pnpm lint/test/format was not run per repository review rules.

修复后请 request eeee0717 再进行后续 review。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants