Skip to content

sync: upstream GenOffice main (v0.10.x, ~900 commits) into HermesOffice - #117

Open
criptogus wants to merge 910 commits into
mainfrom
sync/upstream-nov06
Open

criptogus wants to merge 910 commits into
mainfrom
sync/upstream-nov06

Conversation

@criptogus

Copy link
Copy Markdown
Owner

Summary

Syncs the fork with genspark-ai/genoffice through 2026-11-06 (~900 commits, ~60 features, ~110 fixes). Includes the rebrand sweep from #116.

Upstream highlights:

  • Legacy-format conversion pipelines: .doc/.dot→docx, .ppt→pptx, .hwp/.hwpx→docx
  • PDF: page-by-page compare, attachments panel, calibrated measure ruler, reduce file size
  • Sheets: real slicer/timeline persistence on save, external-links panel, queries & connections
  • AI providers: SiliconFlow, PPIO, Command Code, CloudGPU, Stability/FLUX, SearchApi, per-model thinking effort
  • Docs: SmartArt, mail merge label/envelope, comment images, custom doc properties
  • Shell/MCP: sheet_links, fill_template, read_docx_properties tools
  • Slides/PDF: print dialogs with driver options, JPG export

Fork layer preserved: HermesOffice branding, updater + ho-nightly pipeline, local-gateway account flow, integrations/skills UI, quick switcher, attachment allowlist + byte-flow IPC, ZeroAgency footer, i18n shards, Linux packaging, Hermes-named font/metric assets.

Test plan

  • npm run typecheck — all 31 workspaces green
  • npm run lint — 0 errors
  • npm run format:check — clean
  • npm test — all suites green locally (~17k tests; docs 5935, sheets 3987, shell 1270, slides 1579, pdf 1144, markdown 1468, html 366, plus all packages)
  • npm run build:all — all 8 apps + CLI + shell build
  • tools/rebrand-hermesoffice.py --check — 0 violations
  • CI: test + e2e
  • Nightly build validates packaged app

Generated with Devin

tokg-venv and others added 30 commits October 23, 2026 16:00
…nting them (#1884)

* feat(sheets): hide cells from the model

Rectangles in a custom part rather than in the sheets, so nothing in the grid
has to change and the values survive for any other tool that opens the file.

See the PR body for the trace tools, the derived-value guard, and the dialog's
overclaim.

* fix(sheets): keep redaction fixtures and menu label out of CJK

The right-click label was a hand-written 14-language table that duplicated the
shard key redactMenuLabel and missed 7 locales. Read it out of the app's own
string instead; the wording is now defined once and the test compares against
that table rather than a literal, so it cannot rot when a language is edited.

The prompt's worked example shipped Chinese inside an English instruction, and
that string goes to the model verbatim — it is product output, not a fixture.
Latin example now, with a guard so the shipped wording cannot drift back.

Tests and the e2e spec move to a Latin fixture. consolidate/advanced-filter/
create-univer were prettier-only churn: token-identical to upstream once
whitespace and trailing commas are normalised, so they are reverted rather than
resubmitted.

* fix(build): same workspace-dep fix as the core branch

apps/sheets and packages/ui declared @genoffice/agent-core as
`workspace:*`, which npm here rejects — `npm ci` died with
EUNSUPPORTEDPROTOCOL before any code ran. Every cross-package dependency
upstream is `"*"`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(sheets): the guard's parse refusals log at debug instead of vanishing

Carries the shared core commit: the empty-container size fix that stops
selection offsets shifting across an empty paragraph (inherited by every
branch in the stack).

* chore: retrigger CI (the push event was swallowed)

* fix(gateway): redactionStates goes last in planCellEditsToXlsx

The slot was taken before `tabColorStates`, so every caller written
against upstream's order had a parsed table edit arrive as a tab colour:
`xlsx-table-edit.test.ts` failed 12/12 with `TypeError: Cannot read
properties of undefined (reading 'trim')` at `toArgb`.

Appending it also means the placeholder run in `applyCellEditsToXlsx`
needed two more empty arrays — it had four for six unexposed slots, so
`redactionStates` was reaching `tabColorStates` from that side too.

* fix(sheets): the sidecar search never asked the redaction index

`findWorkbookCells` has four paths that can return a cell: the demo
snapshot, the journal, the range scan and the Rust sidecar. The first
three now skip a withheld cell. The sidecar did not, and `shadowed`
could not stand in for it — that set holds only cells edited this
session, so a mark made weeks ago reached the model untouched.

One line, same shape as the other three. The test drives the sidecar
path directly, which nothing did before.

* fix(sheets): marks follow the rows the same save inserts

A mark names a cell in the FILE, but the marks were recorded in
whatever coordinates the grid was showing. Insert two rows above B2,
save, and the part still said row 1 — which the grid XML that same save
wrote had already moved to row 3. Reopen and the reader's hidden cell
is exposed while an unrelated one stays hidden.

Two ends, one conversion between them:

The renderer now records new marks through `screenToFile`, which is the
inverse of the `fileToScreen` replay the index already does, and the
clear gesture translates before it looks the mark up — it arrives from a
click on the grid, so both sides needed it. A row inserted this session
has no file coordinate yet; that mark is refused with a message rather
than written down wrong.

The gateway replays the same save's `structuralOps` onto the marks with
the `shiftCellArea` helper every other sheet-scoped reference uses. A
mark whose rectangle the ops delete goes with them; one they only
partly take keeps what survives.

* fix(sheets): keep the marks when the reader dropped the part

The redaction part is not one every program keeps. LibreOffice rebuilds
the package from its own model and discards parts it does not model,
along with their relationships and content-type overrides -- verified:
soffice --convert-to xlsx over a workbook with marks leaves none of the
three. A user who round-trips through it came back to a file where the
marks existed only in the renderer's memory, and one edit plus a save
handed the model every value they had chosen to withhold.

So every save now also writes the marks into the workbook's defined
names -- the one carrier a spreadsheet program has to keep working. The
name stays inside Excel's narrow name grammar ("Client Phone" is not a
legal name); the payload rides in the formula, which is free text, as a
string literal so it parses everywhere. Names are hidden, so they never
appear in the Name Manager and so the declared-names rewrite leaves them
alone, and workbook-scoped, so deleting a sheet cannot renumber them.

The sheet is recorded by position, not by name. That is the accepted
cost: a workbook reordered outside GenOffice can point a mark at the
wrong table. It fails in the safe direction -- withholding the wrong
table is something the reader can undo, whereas withholding nothing is
the leak.

The part stays the record and wins whenever it can be read; the mirror
is consulted only when the part is missing or unreadable. An unreadable
mirror is still unreadable rather than "nothing is withheld". Opening
such a file and saving it writes the part back from the recovered marks,
so the fallback repairs the workbook instead of becoming a mode.

Verified on real bytes: save with marks, convert with LibreOffice, and
the marks read back out of the output with the label intact.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Excel shows a picture in a note by painting it onto the note's shape as a
picture fill — a `<v:fill type="frame">` carrying the image relationship
on `o:relid`, resolved through the VML part's own rels. The writer could
keep such a picture (the note's shape is rebuilt from the file's own fill)
but could not put one there.

A note that brings a picture of its own now gets the media part, the
relationship that reaches it, and the fill its shape paints it with. The
relationship is allocated in the VML part's own id space: the worksheet's
rels is a different space, and a relationship written there would leave
the fill pointing at nothing.

The shape is given the picture's own size (96 px per inch, 72 pt per inch,
scaled down to Excel's 255 pt ceiling). A fill is stretched to the frame it
paints, so the text box's 108×60 would squash the photograph into it — and
the same goes for a picture the note was already carrying, whose frame is
now kept as it was.
* feat(xlsx-gateway): a note can carry a picture

Excel shows a picture in a note by painting it onto the note's shape as a
picture fill — a `<v:fill type="frame">` carrying the image relationship
on `o:relid`, resolved through the VML part's own rels. The writer could
keep such a picture (the note's shape is rebuilt from the file's own fill)
but could not put one there.

A note that brings a picture of its own now gets the media part, the
relationship that reaches it, and the fill its shape paints it with. The
relationship is allocated in the VML part's own id space: the worksheet's
rels is a different space, and a relationship written there would leave
the fill pointing at nothing.

The shape is given the picture's own size (96 px per inch, 72 pt per inch,
scaled down to Excel's 255 pt ceiling). A fill is stretched to the frame it
paints, so the text box's 108×60 would squash the photograph into it — and
the same goes for a picture the note was already carrying, whose frame is
now kept as it was.

* feat(sheets): put a picture into a note

Univer draws a note as a yellow popup whose body is its text, and that
popup is not something a host can put an image into — so the picture is
journalled and the save paints it onto the note's shape (the gateway half
lives in the core PR).

Review ▸ Notes ▸ Insert picture picks a file for the note at the
selection. A cell with no note gets one: a note with nothing but a
photograph is what Excel's own picture-in-a-popup is, and the genealogy
sheets and old-photo sheets the feature is for are exactly that.

The picture cannot be shown inside the note while the app is open, so the
Note pictures pane shows it instead: the pane now lists the pictures put
in this session as well as the ones the file carries, and offers to take a
picture off a note.
…By list (#2194)

The four statistical aggregations sat in the Summarize By dropdown as
disabled "not available yet" entries while the engine had none of them.
Both halves land together:

- the aggregation envelope (PIVOT_AGGREGATIONS) grows the four names,
  which flows into the workbook-DSL add_pivot schema automatically
- the refresh engine and the bake layout carry the same arithmetic:
  sample statistics divide by n-1 (STDEV/VAR), population by n
  (STDEVP/VARP); non-numeric cells drop out like every other
  aggregation. A single-value bucket is blank where Excel shows
  #DIV/0! — the engine expresses error cells as blanks everywhere
- the OOXML writer maps them to their spec dataField@subtotal names
  (stdDev/stdDevp/var/varp), and the parser now accepts files that
  carry them instead of marking the pivot unsupported-for-recompute
- the four dropdown entries flip to supported (labels already existed,
  greyed); new-field captions follow Excel's own prefixes (StdDev of…)
- Show Values As stays untouched (a separate engine surface)

Tests bake all four over {10,20,30} (var=100, stdDev=10, varp=200/3),
a single-value bucket, and text-cell coercion.
Home › Superscript / Subscript stored a whole-cell vertAlign (the button lit
up and the saved file carried <vertAlign>), but the grid never showed it:
Univer's plain-text painter builds the font from ff/fs/it/bl/ul/st/ol/cl and
leaves `va` out, so the glyphs only shrank and stayed on the baseline. The
document layout path does honor `va` on a text run, and rich-text and rotated
cells already take it — hand the font cache a rich-text twin of a plain `va`
cell so it takes that path too. The cell model is untouched; files that
already carry a whole-cell <vertAlign> render correctly on open as well.

Inside the cell editor the buttons now format the selected characters
("1st" with a raised "st") through Univer's inline-format command instead of
restyling the whole cell behind the editor, like Excel. The gate is the edit
session bit (EDITOR_ACTIVATED), not the DOM-focus bits the clicked button
has just taken.

Closes #2212
…olumns (#2210)

Co-authored-by: Aniruddha Adak <aniruddhaadak80@users.noreply.github.com>
The outline view drew a ⊕ handle on every heading with a ::before
pseudo-element and no click handling — a decoration that looked
interactive and did nothing. The handles are now real widgets from an
outline-view extension: click folds or unfolds the heading's descendant
heading rows (Word's outline semantics — the view already hides body
blocks), ⊖/⊕ tracks the state, leaf headings show a dimmed inert
handle, and the collapse set is keyed by level+text like the navigation
pane so folds survive unrelated edits. Toggles are meta-only
transactions (no undo step, no dirty), decorations exist only while the
view mode is on, and leaving the view clears everything.
…e save (#2182)

imagePatchOf (original-document images) carries wrap and placement, but
the genImage save branch re-serialized only width/height/align/z-order —
a newly inserted picture set to float and dragged silently snapped back
on reopen, and the Layout ▸ Position grid stayed disabled until the
file had been saved once (ribbon-layout-tab said so in a comment).

The save path now re-derives the floating payload from the live node
attrs — the same place wrap/drag/preset edits land — into posOffsetEmu
or the margin-relative posH/posV pair, and clears stale fields so back-
to-inline drops the anchor. NewImage grows the optional posH/posV pair,
which patch.ts feeds into applyImageWrap's marginAlign: the same bytes
imagePatchOf writes for originals, without baking a preset into absolute
offsets. The Position grid is enabled for new images (canPosition no
longer requires docxIndex), and the paste-a-float copy path, which had
the same silent loss, goes through the same helper.
…act (#2185)

savePptxToFile opened the deck's own path with createWriteStream, whose
default 'w' flag truncates it before the first chunk lands, so for the
whole stream the .pptx on disk was empty or partial, and a crash, kill
or power loss in that window left it that way with no copy to fall back
to. Only the password path was safe: it already went through
atomicWriteFile.

electron-utils gains atomicWriteVia(filePath, write): the writer fills
the sibling temp, which is then synced and renamed over the target with
the same retry and in-place fallback as atomicWriteFile; a writer that
throws part-way leaves the target untouched. The four deck saves in
Slides (Ctrl+S, Save As, autosave, the MCP save) go through it.

Fixes #2123
…xes, shapes and lines (#2178)

View → Slide Master already parsed and edited master/layout parts (select,
text, transform, fill, stroke, delete) but offered no way to add anything,
so a master whose deck needed a new logo box or footer note was a dead end
(#2177). The master bar now carries the same insert tools the slide canvas
has:

- a Text Box tool (click-to-type box, straight into typing) and a Shapes
  gallery (the shared ShapeGalleryContent, lines included) — picking one
  turns the canvas into the same crosshair draw mode slides use, with the
  click-or-drag sizing and Shift constraints from draw-shape
- a part-addressed masterAddElement IPC that runs the slide path's own
  addElement op against the session's current master/layout part, so undo,
  the journal and the byte-surgery write-back all behave like the existing
  master edits

addElement opts into part targets (allowPart, like setText/setFill), which
also makes 'genoffice slides apply --ops' able to insert onto master and
layout parts; resolvePart's parse-on-demand covers unseeded parts. New keys
are translated in all 21 app locales, reusing the per-locale ribbon wording.

Fixes #2177
The AI page builder was the one editor whose panel could not fetch or
make a picture: the IPC plumbing existed (html:ai-generate-image handler,
preload exposure) but no tool reached it, and ai:image-search had no
main-process handler at all, so the skill prompt told the model to ask
the user for attachments instead. Docs, sheets, slides, markdown and pdf
all ship this; html now does too.

- ai:image-search (and ai:web-search, the pair every sibling registers)
  registered for standalone html windows; shell-tab mode already shares
  the docs-side registration
- search_images and generate_image tools mirror the markdown/docs shapes,
  gated by the same imageGenerationAvailable capability predicate; an
  off provider hides generate_image behind the shared off-note
- generated bytes land in assets/ via the existing saveImage path (data
  URL fallback when the page is unsaved, hotlink fallback when the
  download fails), insertion goes through the ribbon's own insert op
- the html skill prompt now describes the three real picture sources
- four ai strings translated in all 21 locales
…lying about it (#2196)

An element with text-align: justify made the style panel's alignment
select show "left" — the value list recognized justify but offered no
option for it, so the computed value fell through to the fallback and
the only way back to justify was hand-written CSS. The option and a
fourth button (the docs ribbon's own justify glyph) join both surfaces;
image alignment is untouched — its float/margin model has no justify.
Label translated in all 21 locales.
* feat(shell): OpenAI's GA Decisions API as a decision-model route

OpenAI's /v1/decisions went GA on 2026-10-22 with gpt-6-luna, but it
speaks a sibling protocol rather than TypeSafe's /v1/systemone: shared
evidence in an `input` field (no `state`), questions as an array with
ordered `levels` instead of a criteria map, and `answers` back as an
array keyed by question name. Its score is the probability average of
level indices, so the three System One criteria map onto levels 0/1/2
and the calibrated 0-2 relevance score carries over unchanged.

prepare and validate translate both directions for the new `openai`
route: documents travel once inside the shared input as numbered
<document> blocks with the same anti-injection sentence, and a reply is
validated by name with the same distribution and expectation checks —
refusals, missing levels and drifted probabilities fail the call, and
callers keep the local order as with any malformed reply. The settings
dropdown gains OpenAI (gpt-6-luna), and the endpoint id joins the
shared union, the key-slot defaults and the settings normalization.

* test(e2e): the settings tripwire knows the openai key slot

settings-media-search.spec.ts pins the saved fileSearch shape with a
deep equality on keys — the deliberate-act tripwire for new endpoints.
It predates the openai route, so the saved settings gained a key slot
the fixture did not list and the poll never settled.
Kimi's K3.1 series opened with K3.1 Pro (3T-class parameters, native
vision, 1M context, 2026-10). The direct Kimi provider lists it ahead
of K3 and defaults to it, the same treatment #2100 gave MiniMax M3.1
Flash; K3 stays listed while the vendor still sells it. OpenRouter
listed moonshotai/kimi-k3.1-pro on 2026-10-23, so it joins that catalog
too. Atlas Cloud's seeded snapshot is unchanged — its comment dates the
verify to 2026-10-08 and the id is not on it.

K3.1 Pro takes images natively, so it stays out of modelLacksVision()
(anchored by a test, like the other Kimi ids). The kimi provider's
omitTemperature rule is provider-level and covers the new id as-is.
Orphans of removed or never-shipped features, mirrored in every locale
with zero references anywhere (src, tests, e2e):

- shell: navTrash/navTrashTip (a trash entry the home nav never grew,
  the manual documents the system-Trash design instead),
  secActivity/timelineEmpty/timelineYou/timelineUserAria/daysAgo/
  noContent (an activity timeline that never shipped),
  cloudGroupThisWeek/cloudGroupThisMonth/cloudSortLabel (cloud-project
  grouping remnants), accountGenspark, setMcpConn, setAiImageModel,
  setAiAnalysisModel
- sheets: appComingSoon (a "coming soon" placeholder whose entry point
  never existed)

Key sets stay symmetric across locales — the shell string table and the
sheets app dictionaries are both typed against their zh definition, so
typecheck is the parity gate.
…ot, in all 21 languages (#2167)

- ODF import (open-only, #1982), hide-from-AI ranges with the app-only scope
  note (#1883, #1884), Mailings merge tab (#2122), content controls (#2118),
  the docs mind map (#1999), in-cell pictures as rich values (#2042),
  3D models (#2091), narration recording (#2056), wiki links / backlinks /
  graph (#1977, #1996, #1998), Ask AI attachments, conversations and CLI
  artifact cards (#2068, #2069, 58dfd0b), docs compare in the CLI (#2119),
  the search-backend list (#2124) and a FAQ on hiding the AI sidebar (#1951).
- Every feat commit and sync snapshot in aff3d20..upstream/main reviewed;
  below-README-granularity items deliberately left out; no stale claims found.
- Translations mirror the same twelve changes and are machine-authored;
  a native-speaker pass is welcome. Ask AI additions land only where the
  post-10-13 section exists (English, vi).
Co-authored-by: Aniruddha Adak <aniruddhaadak80@users.noreply.github.com>
…ads (#2205)

* fix(electron-utils): return null for malformed percent data-URL payloads

* style: prettier

---------

Co-authored-by: Aniruddha Adak <aniruddhaadak80@users.noreply.github.com>
* fix(docs): escape caption labels in the SEQ field matcher

* style: prettier

---------

Co-authored-by: Aniruddha Adak <aniruddhaadak80@users.noreply.github.com>
…#2207)

Co-authored-by: Aniruddha Adak <aniruddhaadak80@users.noreply.github.com>
…ions (#2213)

GFM footnotes were listed in the AI prompt's ban list because nothing parsed
them: a reference stayed literal text and a definition line rendered as an
ordinary paragraph (or vanished as a link reference definition when its body
was a single word). Both halves now parse into their own nodes.

- footnoteReference: an inline atom showing the label as written; the input
  rule converts a typed `[^label]` (not inside code or a bracket).
- footnoteDefinition: a top-level block in the editor's own `[^label]:`
  rendering, with indented continuation lines, blank-line-separated second
  paragraphs and lazy continuations included in the body like cmark-gfm.
- The block tokenizer rides the markdownTokenizer seam; the opening regex
  judges the first line alone because `$` cannot see past a newline, and the
  raw ends after the last kept line so the block source map still tiles the
  source exactly — an untouched footnote file saves byte-identical, and an
  edited one re-serializes only its own block.
- Editing: clicking a reference jumps to its definition and back; /footnote
  inserts a reference plus an empty definition at the end and puts the caret
  in it; Enter in a trailing empty paragraph leaves the definition instead of
  nesting another one (a definition left with a single empty paragraph
  becomes that paragraph).
- The AI prompt moves footnotes to the allowed list with the pairing rule.
- docx export keeps the note visible as a `[^label]: ` paragraph (docx has
  its own footnote model; mapping it is future work) and the reference as a
  text run; print HTML prints the editor's own rendering.
- insertFootnote localized in all 21 locales.

Tests cover parsing shapes, round-trips (continuations, escapes, code spans,
fenced blocks, orphaned halves), the probes, byte-identical saves through the
block source map, the Enter exits and label allocation.
…itors (#2195)

Entering a text box greyed out the ribbon's list and indent buttons —
Word formats lists inside boxes (cover pages, sidebars) all the time.
The paragraph's list identity now rides on its existing listMarker
model end to end:

- parse fills the marker with the paragraph's w:numPr identity
  (numId/ilvl/kind), so a listed box loads with its list attached
- the Home bullets/numbering buttons unlock for a focused box: toggling
  writes the marker (kind + a document numbering numId, allocated by
  the same machinery the body uses) and Enter continues the list —
  splitBlock carries the attrs; a small guarded plugin renumbers
  ordered markers per box on every doc change (counters restart at
  deeper levels like Word)
- indent steps the paragraph's own indentLeft (720 twips per click,
  floored at 0) — textboxes have no style/grid interactions
- the save diff's signature gains list identity (numId/ilvl/kind; the
  marker text stays out — both sides recompute per-box counters), so a
  toggled list reaches the byte patch even with untouched runs; the
  patch writes or clears w:numPr in pPr, placed after w:pStyle per
  CT_PPr order, and the ribbon's active state reflects the marker kind

The list library galleries, multilevel list and paragraph styles stay
main-document only (their machinery targets body blocks). Engine tests
cover numPr insert/replace/clear/placement; marker fixtures gain the
identity fields; the signature tests pin identity-in/text-out.
…eight (#2154)

* fix(docs): a merged group crossing a page keeps its rows at content height

A table with vertically merged cells that crosses a page boundary
rendered later group-head rows at page-tall heights — one line of text
and then blank space (genoffice#2114). Three defects fed one another:

- The measure pass hides gap widgets with display:none, which removes a
  gap table row from the table grid entirely: a rowspan crossing it then
  extends over more rendered rows, every later row's cells shift one
  column, and a fixed-layout column collapses to zero width. Its cell
  wraps one character per line (~20 lines per list item), so the row
  measures page-tall. The gap row now stays in the grid at its real
  height — domTableRows' gapAbove already subtracts each gap from the
  rows below, and the block height is tiled gapless so the last row does
  not absorb the gaps above it.
- singleCutCell anchored a full-height in-cell gap band inside a merged
  row's cell (its height shifts every row the merge covers, feeding the
  next measure). A row inside a vertical merge now takes the zero-height
  cut marker.
- _placeTable charged vMerge continuation rows to no column and never
  turned a page on them, so computed boundaries drifted past the real
  DOM heights. They are placed and fitted like any other row now; the
  parity model's zero-height continuations are unchanged.

The e2e spec inserts the issue's shape (rowspan-7 groups, ordered-list
description cells, explicit column widths) and asserts what a reader
would check: every row stays under its content height, the table spans
several pages, and no gap band lands inside a merged table's cells.
Both the css and the singleCutCell mutations turn it red (5007px rows /
2 bands).

* Merge upstream/main: restore styles.css untouched (the collapse fix subsumes this branch's css)

* style: drop the unused rmSync import

* fix(docx-engine): re-export the resolve-props resolver from the barrel

#2118's resolve-props.test.ts and apps/docs' convert.ts both import the
resolver (resolveCtxOf / resolveParagraphPropsXml / resolveRunProps and
the EffectivePPr / EffectiveRPr / ResolveCtx types) from
'@genoffice/docx-engine', but the barrel never re-exported the module —
internal callers go through './resolve-props' directly — so tsc fails
on the missing members, and every open PR with it, since the
pull_request typecheck runs on the merge ref. Add the re-exports.

* chore: drop a stray issue-comment draft that was never meant to ship
… (#2193)

* feat(pdf): a comments panel that lists every note across the document

Comments only existed as pins in the page margin — after a multi-page
review there was no way to see them all, find one, or jump back to it
(Acrobat's Comments panel is the reference).

- the View tab gains a Comments toggle beside Outline; the pane lists
  every thread root grouped by page order, with author, a one-line
  snippet and the reply count, and a text filter matching thread texts
  (replies included) and authors, case-insensitively
- clicking a row scrolls to the page and opens the thread popover at
  its pin, the same activation the margin uses
- the list is built from the same thread source as the margin
  (noteThreadsFor: the visible-page cache plus one getAnnotations per
  uncached page), and rebuilds when notes are added, deleted, edited
  or replied to in this session
- four new strings translated in all 21 app locales

Pure logic (filter/order/reply count) is exported and unit-tested:
filter matches reply contents and authors, pages order ascending with
time as the tiebreaker (unknown times last), and the input array is
never mutated.

* style: comments panel colors go through the theme tokens

The theme-colors gate rejects raw fallbacks in renderer CSS; the hover
uses --hover and the secondary text --text-secondary like the outline
pane beside it.
Links in the file could be clicked but never created or changed — the
Edit tab had no link tool and the save pipeline had no link writer.
Acrobat's Edit PDF > Link is the reference; this brings both halves.

- Edit > Link arms a drag-a-box gesture on the canvas (same draw
  machinery as rect annotations); the drawn box opens a small dialog
  taking exactly one target — a web address or a page in this document
- a new 'link' drawing kind carries the box through the edit ops
  (validate: exactly one of url/destPage, destPage a 1-based integer);
  save writes it as a Link annotation: /A /URI for web targets, /Dest
  [page /XYZ null null null] for in-document jumps, no appearance stream
  (viewers draw their own hover affordance, and the read path — pdf.js
  getAnnotations, which feeds LinkLayer — surfaces url/dest from
  exactly these fields, so a saved link renders and jumps on reopen)
- pending link boxes preview as dashed blue rectangles on the page and
  are selectable/deletable like other pending drawings
- five new strings translated in all 21 app locales

Tests: writer round-trips for both target kinds (pdf-lib asserts the
annotation bytes, pdf.js asserts the read path returns the url) and
edit-ops validation cases (neither/both targets, non-positive pages).
The PDF search had no options; Acrobat's carries both. Two toggles join
the search bar (Aa / W, aria-pressed, activate-state styled like the
existing selection popup), and searchInIndex takes an options object:
match case scans the raw text (the case-folded copy stays for the plain
path), whole word asserts a word boundary on each side — CJK neighbors
count as separators and CJK match edges impose none, so a CJK query
degrades to plain matching while a Latin word glued to CJK is still
rejected. Toggling re-runs the search like editing the query does.
Both labels translated in all 21 locales.
…eading styles (#2215)

The generator writes a heading as a reference to its level's heading style
(ctx.headingStyleIds), and the editor's gallery, the shortcuts and the AI ops
leave styleId empty for a level the document does not define. In a document
whose styles.xml has no "heading N" — converter output, lean generators —
the heading therefore saved as a bare <w:p> and reopened as body text, which
the editor's autosave round trip showed as the heading "reverting" a moment
after it was applied.

saveDocx now creates Word's built-in definition (the same one the blank
template ships: bold, keep-with-next, outline level, quick style, based on
the document's default paragraph style) for each missing level and
references it. A level the document does define — under any id, localized
names included — keeps its own style; outline-only headings and headings
with an explicit style are untouched. Built-in definitions are created
without w:customStyle, as Word writes them.

Refs #2211
An unresolved [[link]] was a dead end: the suggest menu showed "no
matches" and clicking the red link only toasted an error. Obsidian
offers creation at both spots; this closes the loop the same way.

- the suggest menu grows a Create-new-note row (last, or alone when the
  query has no matches); picking it creates the note, completes the
  link with the sanitized stem actually on disk, and opens it
- clicking an unresolved link creates and opens instead of toasting;
  unresolved embeds keep the toast — a missing ![[x.png]] should not
  become an empty .png
- placement mirrors the resolver's own lookup order: a target with a
  directory is vault-rooted (folders created as needed, ../ and
  absolute rejected like escapesVault), a bare name lands beside the
  open document; existing files are opened, never overwritten
- markdown:create-wiki-note IPC; suggestCreateNote translated in all
  21 locales
tokg-venv and others added 30 commits November 4, 2026 15:53
* build(shell): an arm64 pass for the Linux release targets

GENOFFICE_LINUX_ARM64=1 mirrors the Windows arm64 flag: a second
electron-builder pass (own BUILD_DIR) that switches AppImage/deb/rpm to
arm64, taking the sidecar from an explicit cross-compilation triple
(aarch64-unknown-linux-gnu) or from a native arm64 Linux host's plain
build. The x64 pass is byte-for-byte unchanged.

beforePack now checks the ELF e_machine of the Linux sidecar for both
architectures: electron-builder exits 0 on a missing or odd
extraResources source, so until now an x64 host building an arm64 package
(or the reverse) shipped a sidecar Sheets could not run.

* build(shell): fix the stale sidecar-candidate name in the Linux guard

The relocation of the arm64 candidates block renamed the array but left
the beforePack error message pointing at the old identifier.

* build(shell): make the Linux arm64 support reviewable

Answers the CHANGES_REQUESTED review:

- Buffer now comes from node:buffer instead of the global: the
  packaging config is evaluated inside a vm sandbox (the packaging
  tests) that has no Buffer global, so the ELF guard blew up there.
- The linux sidecar candidate list is picked by GENOFFICE_LINUX_ARM64,
  so the missing-source message names the x64 candidates for the x64
  pass and the arm64 candidates for the arm64 pass; it used to print
  the arm64 ones for both.
- The flag is config support only: no CI job runs a second Linux pass,
  so the comments no longer claim one does. Whether Linux arm64 joins
  the release matrix stays a maintainer decision.
- The packaging test's fake node:fs now serves a 20-byte ELF header
  (openSync/readSync/closeSync), and the suite covers the x86_64
  header, the aarch64 header under GENOFFICE_LINUX_ARM64=1, both
  architecture mismatches and the per-pass missing-source message.
…son; dsh install accepts an empty flow sequence (#2491)

The save's formula-cache comparison only let a computed blank or error
replace the file cache when the same sheet carried a value edit, so a
dependent on another sheet kept its stale cache. The flag is now
workbook-wide and counts formula entries, as the recalc overlay does.

The dsh Cordis patch is often initialised as `[]`; cordisIsSequence now
accepts it and the upsert replaces it with the insert block.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…r next wiki link (#2496)

Co-authored-by: Aniruddha Adak <aniruddhaadak80@users.noreply.github.com>
…#2495)

The opener lived inside @genoffice/doc2docx, so the .ppt importer had to depend on the .doc importer to reach it. It is format-independent (the 8-byte signature, the 512-byte header, the input and stream caps, owned stream bytes), so it now lives in @genoffice/compound-file and both importers use it directly.

No behaviour change: the same checks, the same caps and the same failure codes, with the .doc and the .ppt layer keeping their own format checks (the WordDocument stream, the PowerPoint Document stream and the encryption token).
… (#2504)

The AI backends section gains the providers that shipped since the pages
were last synced: SiliconFlow, PPIO and Command Code join the bring-your-
own-key model list (with the Opper / Cheaper Inference / Atlas Cloud
entries three locales had drifted behind on), Stability and FLUX join the
image-generation list, and SearchApi reaches the per-language search
lists. The Skills section documents the catalog's All tab and localized
descriptions, the Sheets section notes that a cell note can carry a
picture shown under its popup, and the Linux install notes point
Chromebook users at docs/platforms/chromeos.md.
Insert > Page Number wrote the legacy '#' placeholder instead of
PAGE_MARK. The header/footer strip editor only turns PAGE_MARK into a
field, so editing the area showed '#' as text, committed
pageNumber: false, and the save wrote a literal '#' with no PAGE field.

The Insert tab and the Header & Footer tab now build the value through
one helper, hfPageNumberOnly, which uses PAGE_MARK.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…sults (#2500)

The stream marks the workbook fully loaded and only then asks the stream
hold for the full recalculation, which it releases a second later. The CSV
export, the ribbon PDF export and the Print dialog checked preloadComplete
alone, so an export right after the load wrote empty formula cells for a
file saved without cached values. The headless PDF export already forced a
calculation pass and waited for it; that helper moves to formula-settle.ts
and the three interactive paths use it too.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Rebase the actions branch onto main after #2343 landed: the panel, its
session-bound links IPC, the panel overlay and the no-edits save shape now
come from main, so this branch keeps only the actions delta.

- a file-level usage scan (sidecar read_external_link_usage) so Break freezes
  every referencing cell at its cached value, shared formula instances and
  error caches included
- Break and Change Source in the links panel: the literals journal with its
  failed-save rollback, and the gateway applying the break/retarget on save
  with the same defined-name/chart transforms as the CLI
- Change Source's file picker and the cross-workbook range picker
- externalLinkOps through the save request, counted as pending work so an
  ops-only save still reaches the file

Co-authored-by: tokg-venv <tokg-venv@users.noreply.github.com>
* feat(pdf): the print dialog offers a printer's driver options

A driver advertises option groups per queue (lpoptions -l: quality,
color mode, media type — this repo's lasers expose all three). The
print dialog now queries the chosen queue and shows those groups as
dropdowns, seeded at the driver's starred default; what the user picks
rides the job as charset-guarded name/value pairs and reaches lp as
-o name=value on the CUPS routes. Values not in the advertised list
and names outside [A-Za-z0-9_.+-] never become spawn arguments, and a
queue that answers nothing simply adds no dropdowns.

The groups shown are the ones the dialog knows how to present
(cupsPrintQuality, ColorModel, MediaType — copies/sides/duplex are the
dialog's own job options and PageSize stays the document's paper);
labels are the driver's own translations, so no UI strings ship with
this. Windows keeps the existing silent-print path, which ignores
these options, so the dropdowns only render on the macOS CUPS routes.

* fix(pdf): tie the driver-options request to the queue it was issued for

Review follow-up: the printer select's printerOptions() promise was applied
whenever it resolved, so a fast A->B switch could show A's driver-option
groups under B. Bump a request id on every picker change and only apply the
answer while it is still the newest request.

---------

Co-authored-by: tokg-venv <tokg-venv@users.noreply.github.com>
…range pick arms only the chosen tab (#2506)

* fix(sheets): usage scan answers in the links envelope; pick arms only the chosen tab

Follow-ups to the genoffice#2350 backflow review: the sidecar returned
the usage scan as a bare array while the preload parser expects
{ links }, so Break threw before journaling; and the cross-workbook pick
armed every other tab, so a range clicked in an unrelated workbook was
formatted as a reference to the selected link.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit 9b931050d49affe2a252c74fdacc1a5a7731ffd7)

* fix(sheets): re-arming a workbook pick cancels the previously armed tab

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
(cherry picked from commit e7af2d52dec2866f2edaf0e0d32340facb84173f)

---------

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
* fix(pdf): keep lines a wrap cannot explain out of one text block

Paragraph grouping glued separate lines into one block when leading, font
size and width matched: list items of similar width, a paragraph opening
with an indent after a nearly full line, a same-size heading in another
font, lines at unrelated left offsets, and paragraphs separated by spacing
under 45% of the leading. Editing any of them then re-wrapped the whole
group, moving the untouched lines.

A line now starts a new paragraph when it opens a list item, follows a
lone first line in another font, or (left-set text) leaves the body edge
of the paragraph. The leading outlier split fires at 1.25x the median.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(pdf): split on a font change only when the first line stops short; escape the Han test text

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* feat(sheets): persist slicers and timelines across save

Insert > Slicer/Timeline worked in the session, but the controls
vanished on reopen: nothing wrote the x14/x15 parts, so a save kept the
pivot's filtered state and dropped the controls. The session state now
materializes on save as xl/slicerCaches/slicerCacheN.xml (pivot
binding + member selection), xl/slicers/slicerN.xml (the control:
name, caption, geometry, column count) and the x15 timeline pair, wired
through worksheet rels + the x14/x15:slicerList worksheet ext entries,
workbook rels + the x14/x15:slicerCaches workbook ext entries, and
[Content_Types].xml overrides. Opening a file that carries those parts
restores the panels, keyed by file identity so a re-save updates the
parts in place instead of duplicating them; deleting a control drops
its parts and references.

The reader keys on container element names and relationship types
rather than ext URIs, so a URI mismatch between writers degrades to
'control not restored' rather than corrupt state. Excel-side fidelity
of the ext URIs is best-effort: a real-Excel round trip cannot be
verified in this environment.

* fix(sheets): a save before the redaction part is read no longer wipes it

The save request carried redactionStatesRef.current verbatim, which is []
until the file's own part has been read — and the gateway treats a
non-null empty array as 'clear the part'. A save landing in that window
wrote a file without the withheld-cell marks it already carried.

Gate on the loaded baseline instead: null until the read lands, exactly
what the pre-slicer code did.

* fix(sheets): the slicer XML shapes are the ones Excel requires

The slicerCache bound its pivot through a home-made tabId (the 1-based
index of the pivot part among sorted paths) and no name; MS-XLSX says
tabId is the hosting worksheet's sheetId and name is the pivot table's
own name, and Excel does not bind the slicer without them. The host
worksheet is found through its pivotTable relationship and its sheetId
read from the workbook.

New slicer parts now carry the required rowHeight (Excel's default), new
timeline parts level/selectionLevel (month, our model's granularity).

Timelines were wired as x15 table-slicer extensions; they have their own
shapes — x15:timelineRefs on the worksheet and x15:timelineCacheRefs in
the workbook, each with its own URI — and the timeline cache state now
carries the refresh versions, the bound pivotCacheId, the filter type
and the spec's <bounds> (the reader accepts both shapes, so files
written by the previous build still restore).

Tests seed the pivot hosting relationship and assert the new shapes.

* feat(sheets): slicers and timelines get their on-sheet control back

The parts and bindings were written, but no drawing graphicFrame, so
Excel opened the file with nothing visible on the sheet. Each control now
gets a twoCellAnchor holding sle:slicer (or tsle:timeslicer) in the
worksheet's drawing part, created and wired on first use. An anchor that
already names the control is left untouched, which is how Excel-authored
geometry survives a rewrite; a new control lands in the next free slot so
several controls do not stack.

The drawing part goes through the same ensureSheetDrawing used by the
visual-additions pipeline (content type, worksheet rel, <drawing>
element), only exported.

* fix(sheets): slicers land alone, with the parts Excel keeps

Timelines are out of this PR (read and write): their parts ride a save
untouched and the session timeline stays session-only — timeline
persistence follows separately, as suggested.

The slicer side gains the shapes Excel repairs away when missing:

- the bound pivotCacheDefinition carries its x14 ext (pivotCacheId,
  slicerData="1"), and tabular@pivotCacheId equals it — a mismatch cost
  'Removed Part: slicerCache'; removal strips the ext once no cache
  binds the pivot cache anymore (and opens a self-closing definition)
- removing a control drops its drawing graphicFrame too, not just the
  parts/rels/exts — an orphan sle:slicer was left behind
- a hidden Slicer_<name> definedName rides the workbook (removed with
  the control); cNvPr ids are max-plus-one per drawing part; the frame
  is wrapped in mc:AlternateContent with a down-level fallback shape

* fix(sheets): slicer parts Excel keeps — drop the OLAP flag, anchor clientData, own-sheet wiring

* the x14 pivotCacheDefinition ext now carries only pivotCacheId: slicerData
  must be false for tabular/worksheet-pivot sources (MS-XLSX), and the
  OLAP-claiming flag is what made Excel drop the slicer cache — and then the
  drawing — on Save As.
* the drawing twoCellAnchor carries <xdr:clientData/> at anchor level, after
  </mc:AlternateContent>, instead of inside the graphicFrame.
* addSlicerDefinedName opens a self-closing <definedNames/> in place (no
  second container after </sheets>), lands a new container at the
  schema-correct position, stays idempotent across saves, and writes the
  #N/A value Excel writes.
* restoreSlicerPanels binds the panel to entry.sheetId (falling back to the
  pivot's sheet only for unknown ids), so dashboard controls save their
  rel/ext/frame on their own sheet instead of stranding the original.
* Excel-authored slicer and slicerCache parts keep the attributes and
  subtrees the session does not own (style/rowHeight/lockedPosition/xr10:uid/
  extLst; sortOrder/customListSort/showMissing/crossFilter), without
  duplicating crossFilter.

Tests: gateway round-trip assertions for every binding-chain part/rel/ext/
drawing, self-closing definedNames, idempotent saves, Excel-authored
preservation and a two-sheet dashboard; sheets restore tests.

Fixes review feedback on #2226.

* fix(sheets): pin the slicer ext URIs and keep Excel's x14 cache ids

* WORKSHEET_SLICER_LIST_URI was a bogus GUID, so Excel treated the sheet's
  slicer-list ext as a foreign extension, never instantiated the slicer, and
  dropped the slicer cache and the drawing on Save As. Use the MS-XLSX 2.2.2
  URI {A8765BA9-456A-4dab-B4F3-ACF838C121DE}; addExtRef/removeExtRef now find
  Excel-authored extLst entries too (no second ext, no dangling slicer r:id).
* x14 pivotCacheDefinition@pivotCacheId is its own id space, not the workbook
  cacheId: the reader resolves tabular@pivotCacheId through each definition's
  x14 ext (workbook cacheId fallback only when no ext exists), the writer
  reuses an existing ext id and allocates max+1 for a new one, and tabular is
  kept consistent with it. Genuine Excel pairings (cacheId 110 / x14 id
  2124480673) round-trip unchanged now.
* drop the tabId sorted-part fallback — tabId is a sheetId, never a cache
  discriminator.
* preserve the file's whole <pivotTables> block on rewrite so an Excel slicer
  connected to several pivots (Report Connections) keeps every pivot.
* timelines are session-only: remove the "persists" claims from the panel and
  state docs, and drop the dead timeline identity fields.

Tests: pin the three ext URIs, Excel-authored extLst add/remove, the x14 id
space (genuine pairing, round-trip shape, legacy fallback, id reuse), tabId
no-false-bind, and multi-pivot <pivotTables> preservation.

Fixes review feedback on #2226.

* fix(sheets): slicer persistence review fixes

- Slicer_<name> defined name no longer carries hidden=1 (Excel repairs
  the slicer cache when it does); tests pin the no-hidden form.
- Shared slicer parts: parse every control (self-closing or paired with an
  extLst child), rewrite only the control keyed by slicer@name and keep the
  others verbatim, append a new control to the worksheet's existing part,
  and on removal strip just that control/frame/defined name while the part,
  rel/ext, drawing and cache wiring survive until the last control is gone.
  Conflict checking is cache-path based so shared-part removal+write works.
- Reader accepts Excel's paired <slicer>...<extLst/></slicer> form.
- Restored captions survive the first rewrite (SlicerUiState.caption from
  the file; save payload uses caption ?? fieldName).
Tests: 6 new slicer-roundtrip cases; sheets restore tests cover the paired
form and a custom caption.

* fix(sheets): slicer definedName follows the cache name; allow remove+add in one save

Review follow-up:
- One definedName per slicerCache, named like slicerCacheDefinition@name (no
  Slicer_ prefix, no control name/caption). A shared cache keeps its single
  entry; the last control removes it. Excel dashboard caches with their own
  names (e.g. 슬라이서_담당PM) are no longer repaired away.
- allocatePartPath counts removed package paths as taken, so removing a
  restored slicer and adding a fresh one in the same save no longer hands the
  just-removed slicer1/slicerCache1 names back (which put one path in both
  removed and added and tripped assertOnlyTouchedEntriesChanged, poisoning
  every later save).
- Removal matches controls by cache name only; a missing cache part no longer
  strips an arbitrary shared control.

---------

Co-authored-by: tokg-venv <tokg-venv@users.noreply.github.com>
…shift splits (#2513)

A shared formula group stores its formula once, in the anchor; each
follower cell is a bare <f t="shared" si="N"/> whose formula is the
anchor's, translated by the follower's offset from the anchor. When a
row or column insert or delete moves the anchor and a follower by
different amounts, or moves one of the references differently from
the cells, the group stayed shared: only the anchor's text and the
group's ref were rewritten, so the followers silently expanded to
other cells.

Example: D3 holds <f t="shared" ref="D3:D6" si="0">B1</f> and D4..D6
are followers (D4=B2, D5=B3, D6=B4). Deleting row 5 kept the group
shared with ref D3:D5, so the follower now at D5 expanded to B3
instead of B4. Inserting a row before row 5 expanded D6 to B4 and D7
to B5 instead of B3 and B4. A group wholly past the boundary breaks
too when its references straddle it (D10:D11 = B4, insert before row
5: D12 expanded to B5 instead of B6), and so does a group on another
sheet whose references into the shifted sheet straddle the boundary.

#2463 handled only the removed-anchor case, with a copy of the
gateway's edited-group materializer that turned an untranslatable
follower into a constant, against this module's rule that what cannot
be shifted throws.

Before each insert or delete, on the shifted sheet and on sheets that
reference it, a group is now rewritten as plain formulas when its
anchor is removed or when, for any surviving follower, the shifted
expansion would differ from the shifted formula: the check compares
where each reference on the shift axis lands, from the anchor's
references read once per group. Other groups stay shared and
byte-identical apart from the moved addresses and ref. The rewrite
drops t, ref and si from the anchor and gives each follower its
translated formula; the normal reference shifting then runs on plain
formulas. A follower whose translation would leave the sheet throws
StructuralShiftError on this path.

The scan and rewrite are one helper, materializeSharedFormulaGroups,
used by the structural path and by the save path's edited-group case.
The save path keeps its behaviour: a group is rewritten when its
anchor is edited, and an untranslatable follower keeps its cached
value, as its existing test expects. decodeXmlText moves to
xlsx-escapes so both modules decode the anchor text the same way.

Row moves (move-rows) are not changed by this commit.
Univer 0.25.1 already rewrites the formulas inside a sorted range:
FormulaReorderController (@univerjs/sheets-formula-ui, loaded through
UniverSheetsCorePreset) shifts each moved formula by the rows it moved
and writes every formula cell of the range as a plain formula. #2465
added a second interceptor doing the same work because its tests
registered the formula plugins without that controller: with the
controller in their injector, its four tests pass on the code before
#2465.

What the controller leaves wrong is a shared-formula (si) group cut by
the range, with its master inside and followers outside. The master
loses its si, so the followers outside no longer resolve from it: with
D1 =B1*C1 shared down to D8 and rows 1-4 sorted, D5:D8 read =B3*C3 to
=B6*C6 and the edit journal saves those.

The interceptor now handles only that case. It looks for shared masters
in the sorted range; only when one is there does it scan the sheet for
that group's cells outside the range, and writes them as the plain
formulas they showed before the sort. The undo restores their f and si.
Everything inside the range is left to Univer, and a sort with no
shared master in its range no longer scans the whole sheet.

The tests now run with FormulaReorderController, as the app ships, and
cover the cut group, its undo and redo, its journal entries, and a sort
that cuts no group, where the interceptor adds no write.
The PDF app answered its author request with the OS login, so since
#2467 every note, highlight, shape and stamp a save adds carried the
login even when the user had set a name in Settings > General, which
Docs, Sheets and Slides already write.

The handler now resolves the name through configuredAuthorName, the
helper Sheets and Slides use: the Settings name, else the computer
account name, sanitised the same way. The preload forwards the shell's
app:author-name-changed broadcast, so an open document follows a
rename. Notes and replies written by the AI assistant keep their
"AI Assistant" author; every other annotation keeps the user as author,
as #2467 set it.

The Settings description of the field now names PDF in every locale.
…ide (#2516)

Cross-page paragraph stitching (P32) moves the next page's first paragraph
onto the previous page's last one, so that Word flows a paragraph cut by
a page boundary as one. The PowerPoint export ran it too, but each slide is
a positioned copy of its page. Four full pages of running text came out as
slides 2 and 4 empty, and slides 1 and 3 each holding two pages of lines in
the first page's box, at half the line pitch (7.2 pt for 11 pt text). Absolute
layout now skips the stitch, as cell-data already did. Word and Excel output
do not change.

The header comments of the PowerPoint and Excel exports still described
the furniture choice reversed by #2471; they now say what each export does.
Use Brazilian Portuguese terminology across editor messages and help topics, and label the existing pt locale as Português throughout language selectors. Preserve locale identifiers, translation keys, placeholders, commands, and links.

Co-authored-by: Wellyngton Marcondes <admin@simplenetwork.com.br>
* feat(slides,cli): a legacy .ppt opens as a converted .pptx copy

Stage 2 of the ppt2pptx roadmap: wire the stage-1 importer into the two
entry points that were waiting for it.

Slides: a .ppt rides the same import flow an .odp already uses — the
lossy-import confirmation with its skip checkbox, the destination picker
(the sibling .pptx by default), an atomic write of the converted deck,
then the copy opens so every later Save lands there. The old
cannot-open-legacy-.ppt dialog remains for what the importer refuses:
an encrypted OOXML compound file keeps its dedicated message. The import
strings (detail, failure title and body) ship in all 22 app languages,
and 'ppt' joins the per-format skip list.

CLI: genoffice convert deck.ppt --to pptx runs the same importer
in-process, reporting the slide count and the stage-1 warnings (stacked
text boxes; formatting and images are later stages) in the JSON detail,
with the importer's stable error codes on the failure path.

The conversion is honest about what stage 1 does: editable text boxes in
the original slide order and size; geometry, images and text formatting
are declared follow-up stages.

* test(slides): the atomic-write pin follows the ppt import variable

The deck-open-formats test greps the main module for the atomic write of
the converted copy; the ppt route shares that write now and the bytes
variable is named for both sources.
… and pivot-cache exts land in the root's own extLst; timelines do not dirty slicer parts (#2520)

The slicer-state IPC listed every archive name but extracted no
xl/pivotCache part, so the parser saw a definition as present and failed
on readText: restored slicers never came back. The slicer ext and the
pivot-cache x14 ext chose the last extLst in the part, which is a nested
one inside a linked CF rule or a cache field; one root-tail scanner now
serves table parts, slicer wiring and the pivot cache alike. Timelines
are session-only and no longer mark the slicer parts dirty.


(cherry picked from commit dac677e1d84b7fb078062686538f0d41969ca74a)

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
#2514 leaves the formulas inside a sorted range to Univer's
FormulaReorderController. UniverSheetsFormulaUIPlugin registers that
controller but creates it only in onSteady, and the UI renderer reaches
lifecycle Steady 3 s after the first render. A sort in those 3 seconds
reordered the rows with no controller listening, so each moved formula
kept its old references: a row sorted from row 4 to row 3 still read
=B4*C4.

installSortFormulaRefs, which runs when the workbook loads, now creates
the controller; the plugin's own get at Steady returns the same instance.

The tests registered the controller and created it themselves, which is
why they passed. The harness now only registers it, as the plugin does,
so the seven sort tests fail without the fix.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
…s in the drawing font (#2522)

* fix(slides): discover macOS font asset catalogs instead of hardcoding Font7

macOS 26 downloads on-demand fonts (NanumGothic, PingFang, ...) into
/System/Library/AssetsV2/com_apple_MobileAsset_Font8 and leaves the Font7
catalog empty. With Font7 hardcoded, every downloadable face resolved to its
stub subset under FontServices.framework/.../Subsets, which maps only the
glyphs that spell the font's own name (NanumGothic's stub: 45 code points,
no Latin letters, no Hangul text). Layout then measured with heuristic widths
and the renderer drew the missing glyphs in fallback fonts, opening visibly
wide gaps after Latin words in NanumGothic decks.

Discover every com_apple_MobileAsset_Font<N> catalog (downloaded and
OS-preinstalled), newest version first, in both the slides font registry and
the shared font-metrics scanner; the pdf test helper reuses the same lookup.

* fix(slides): measure ground-truth widths with the drawing font stack

Private faces are measured by the renderer's own measureText, but with a
fixed fallback list ('PingFang SC', 'Microsoft YaHei', ...), while the
canvas draws with konva-adapter displayFontFamily, whose fallback depends on
the family (Korean-named faces fall back to Malgun Gothic / Apple SD Gothic
Neo, plus the per-font FONT_STACK entries). Any glyph the face lacks was
measured in one font and drawn in another, so every following glyph drifted.

The renderer now exposes displayFontFamily as
window.__genofficeDisplayFontFamily and the ground-truth script measures with
it, keeping the generic list only when the hook is absent.
…ermesOffice

Help > About showed "GenOffice" in every locale, window titles said
"GenOffice Docs/Sheets/Slides", PDF annotation authors and the AI user
agent still carried the upstream name.

- app-menu: About labels -> HermesOffice in all locales; dialog title,
  message and copy button now use app.getName() so standalone editors
  show their own productName
- docs/sheets/slides/pdf mains: window titles, Help menu labels,
  DOCX-creation error strings and setAiUserAgent -> HermesOffice
- save-pdf: annotation /T author falls back to 'HermesOffice'
- ai-provider: default user agent 'HermesOffice'; codex-app-server
  prompt prose refers to HermesOffice (the <genoffice_payload> wire
  marker is unchanged)
- ai-search: connectivity probe query uses 'HermesOffice'

Kept intentionally: internal identifiers and on-disk/wire markers
(genoffice CLI binary, pipe names, .genoffice-assets.json,
PDFName keys, VISUAL_SIGNATURE_CONTENT_PREFIX) so existing installs,
files and integrations keep working.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
Syncs the fork with genspark-ai/genoffice through 2026-11-06: legacy-format
conversion pipelines (doc2docx/ppt2pptx/hwp2docx), PDF compare + attachments,
slicer persistence, new AI providers, MCP tool additions, and ~100 fixes.

Fork layer preserved on top: HermesOffice branding/names, updater + nightly
pipeline, local-gateway account flow, integrations/skills UI, quick switcher,
attachment allowlist + byte-flow IPC, ZeroAgency footer, i18n shards, Linux
packaging paths, and Hermes-named font/metric assets (GenOffice*.woff2
replaced by HermesOffice* equivalents).

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
…merge

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
- Ask AI (shell): dropped/pasted files now go through files:add-bytes so
  main stages a temp copy and grants read — the old files:add path only
  showed metadata, so thumbnails and model reads silently failed under the
  new attachment grant model.
- Sheets AI panel: same byte-flow fix (upstream left it on the ungranted
  path route); onAddAttachmentPaths/onAddPastedImage props replaced by
  onAddAttachmentBytes.
- codex-history e2e: ai-settings sanitization drops nonexistent cliPath
  values, so a missing path falls back to auto-detection; use an existing
  executable that exits immediately for a deterministic failure.
- home-ask-attachments e2e: assert the staged temp path instead of the
  renderer-supplied original.
- pdf styles: restore token-based .pdf-night background.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The upstream sync surfaces pre-existing upstream text-layout decisions as
'new' lines against our fork base; mark them with the check's own allow
marker rather than refactor upstream code.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
codex-history times out at 90s on Linux CI with zero app output — the
hang is in an unbounded await (firstWindow has no timeout, locator
actions inherit none). Bound firstWindow at 30s with a process-alive
diagnostic, add stage markers and action timeouts to the spec so the
next CI run pinpoints where startup stalls.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
The 30s timer kept running after firstWindow() won the race; firing later
against an already-closed app it threw an uncaught TypeError that
Playwright attributed to unrelated tests.

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
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.