Commit Graph
12 Commits
Author SHA1 Message Date
Martin BallandSkaywalker 9d5924c8b7 speech: Read Aloud on Linux falls back to Speech Dispatcher (#4576)
Lands #4576.

Co-Authored-By: Skaywalker <115165402+Skaywalker@users.noreply.github.com>
2026-11-05 23:36:40 +01:00
Dylan M. Taylor 1a8932b3ed Read Aloud: look citations up by binary search, not a scan per character (#5335)
Building the queue checked every character of the document against every
citation field, so a document with many citations made Read Aloud do
characters x citations comparisons before playback started.

The citation spans are now sorted and merged once, and each position is
looked up by binary search.
2026-11-02 21:16:49 -05:00
aiongg f7c3f3d564 refactor(engine): move speech tests to speech/tests.rs 2026-10-26 20:54:02 +00:00
aiongg 4029a4e6fc Merge remote-tracking branch 'upstream/main' into w1
# Conflicts:
#	ROADMAP.md
#	crates/engine/src/lib.rs
#	crates/engine/src/session.rs
#	crates/mcp/src/tools.rs
#	crates/ui-egui/src/control.rs
#	crates/ui-egui/src/i18n/bg.tsv
#	crates/ui-egui/src/i18n/cs.tsv
#	crates/ui-egui/src/i18n/da.tsv
#	crates/ui-egui/src/i18n/de.tsv
#	crates/ui-egui/src/i18n/es.tsv
#	crates/ui-egui/src/i18n/et.tsv
#	crates/ui-egui/src/i18n/fr.tsv
#	crates/ui-egui/src/i18n/hr.tsv
#	crates/ui-egui/src/i18n/hu.tsv
#	crates/ui-egui/src/i18n/it.tsv
#	crates/ui-egui/src/i18n/ja.tsv
#	crates/ui-egui/src/i18n/ko.tsv
#	crates/ui-egui/src/i18n/nl.tsv
#	crates/ui-egui/src/i18n/pl.tsv
#	crates/ui-egui/src/i18n/pt-br.tsv
#	crates/ui-egui/src/i18n/ro.tsv
#	crates/ui-egui/src/i18n/ru.tsv
#	crates/ui-egui/src/i18n/sl.tsv
#	crates/ui-egui/src/i18n/sr-latn.tsv
#	crates/ui-egui/src/i18n/sr.tsv
#	crates/ui-egui/src/i18n/sv.tsv
#	crates/ui-egui/src/i18n/tr.tsv
#	crates/ui-egui/src/i18n/uk.tsv
#	crates/ui-egui/src/i18n/vi.tsv
#	crates/ui-egui/src/i18n/zh-hans.tsv
#	crates/ui-egui/src/i18n/zh-hant.tsv
#	crates/ui-egui/src/lib.rs
#	docs/gaps.md
#	docs/mcp.md
#	docs/parity-checklist.md
#	docs/ui-parity.md
2026-10-26 03:01:45 +00:00
Martin BallandClaude Opus 5.5 4095cc3413 Security: bounded file reads, Read Aloud and macOS signing temp file (#3680, #3666) (#3877)
* #3680: Read Aloud tidies in one linear pass and cuts space-free sentences

Finding 56. tidy() repeated whole-string replacements until nothing
changed; each pass removed one level of nested empty brackets, so 40,000
pairs took ~50 s in a debug build. It is now one pass that drops a closing
bracket's empty pair as it goes, checked against the old algorithm on
20,000 generated strings. Sentences with no space to split at are cut at
MAX_UTTERANCE (1,200) characters, and each sentence is sliced from a char
vector instead of skipping from the paragraph's start.

Fixes #3680

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

* #3692: Paste Special reads only a regular picture file, bounded

Finding 24. A copied path naming a device such as /dev/zero reports a
length of 0, so the size check passed and std::fs::read read forever.
The path and the opened handle must both be regular files, and at most
the picture limit + 1 bytes are read. A named pipe swapped in between the
check and the open can still block the open (no portable non-blocking
open without platform flags); noted in the code.

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

* #3692: macOS signing import decodes the certificate into a private temp folder

Finding 25. The .p12 bundle was written to a predictable name in the
shared temp folder, where a planted link could redirect it. It now goes
into a mktemp -d folder under the same temp root, removed on exit. The
keychain is unchanged.

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

* #3666: engine file reads open once and check the handle; a FIFO is refused without blocking

read_file_capped checked the path's metadata and then read the path
again, so a file swapped or grown after the check was read whole;
read_file_head opened the path with no type check, so saving to a FIFO
.xml path (the Word 2003 sniff) blocked until a writer appeared. Both
now go through open_regular_file (path checked before opening, handle
checked after, opened once) and a bounded read; read_regular_file uses
the same pieces. Tests: both helpers refuse a FIFO without blocking
and a folder; read_file_capped refuses content over the cap.

Fixes #3666

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

* #3692: Paste Special reads picture files through the engine's bounded read

Ultrareview finding 9: paste_picture kept a third copy of the regular-file
bounded read; it now calls wordcraft_engine::io::read_regular_file (public,
with a wasm stub).

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

* #3666: open a checked file non-blocking so a swap to a named pipe can't hang

CodeRabbit: a path replaced by a FIFO between the regular-file check and
File::open blocked in open, before the handle check could refuse it. On Unix the
open now uses O_NONBLOCK (no effect on regular files; libc is already in the
lockfile) and the handle check refuses the pipe. The FIFO tests' cleanup writers
open non-blocking too, so a regression fails the test instead of hanging it.

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

* #3666: review — Paste Special refuses a picture file whose handle reports more than the cap before reading it

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o

* #3692: review — macOS signing keychain lives in the private mktemp folder too; removed if the import fails

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o

* #3666: review — libc comes from the workspace dependencies

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o

* #3680: review — Read Aloud logs a warning when its sentence cap leaves text unread

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o

* #3692: review — engine speech timing test gets a margin a busy CI machine can't break

tidy_is_linear_on_nested_brackets: 2 s -> 10 s. The one-pass tidy takes ~13 ms on a loaded
machine; tidy_by_replacing (the old tidy, kept in the tests) took 293 s on the same input.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CX1hMKX28764Lrx5moxs1o

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-25 12:32:19 -04:00
+5 462ecc8770 Engine: no dead-code warnings on wasm (#3542)
* fix(engine): do not treat decimal points as sentence boundaries in Sentence case

In crates/engine/src/cmd/format.rs, convert_chars treated any period as a sentence boundary, causing numbers with decimal points (e.g. 3.14, 2.5) to capitalize the subsequent word in Sentence case.

Check if a period is surrounded by ASCII digits, and do not trigger uppercase capitalization when encountering a decimal point.

Closes #3485

* proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too

Review follow-up: the directives that name one flag read the same emoji
form, so they now skip the variation selector like PFX/SFX and stem flags.

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

* fmt: rustfmt equation array centring

* #3108 review: behind-text and in-front-of-text floats don't grow the row

FredApps' review: the row-growth loop counted every non-inline object, so a
page-anchored logo or watermark behind a cell's text would stretch its row to
the object's bottom edge - contradicting table.rs (behind-text frames count
for vertical alignment but don't increase the row's flow height) and Word.
Only floats that take part in the text flow (Square, Tight, Through,
TopAndBottom) grow the row now, the same classification layout already uses
elsewhere for "takes part in wrapping".

New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave
the row at its text height; it fails with the old condition. The existing
Square-wrap tests and cell_frame_centres_empty still pass.

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

* Engine: no dead-code warnings on wasm

`cargo check -p wordcraft-engine --target wasm32-unknown-unknown` warned
about five items whose only callers are platform-gated:
- citations `by_author`, user_templates `MAX_ENTRIES` and `label_of`: used
  only by the desktop (non-wasm) source import and template listing.
- speech `voice_language`: used by espeak (Unix but macOS) and the macOS
  voice picker, so `any(unix, test)`, not `any(not(windows), test)` (which
  also took in wasm).
- speech `primary_tag`: used by those plus the Windows synthesizer, so
  `any(unix, windows, test)`.

Each item now carries exactly its callers' gate rather than a blanket
allow(dead_code). Checked with cargo check on Linux, Windows (gnu), macOS
and wasm32 (zero warnings each), clippy -D warnings, and the engine tests.

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

* Integration: #1847 reject comment IDs above u32::MAX

* Integration: #3267 leave footnote definitions out of link definitions

* Integration: #3471 drop duplicate egui options block; install_ui_fonts sets them

* Integration: #2153 drop trailing blank line and use assert! for bool checks in tab_separated_list_markers

* Integration: #2268 ODT lengths accept only the standard pc unit for picas

* Integration: #2269 LaTeX lengths keep macro names case-sensitive

* Integration: #2673 Khmer substitutes return the (list, flag) pair after #3245

* Integration: #2273 Latin weight/style and line styles take precedence over Asian, complex and line-type attributes

* Integration: #1851 DOCPROPERTY accepts the reference app's LastSavedBy name

* Integration: #1985 only ^0nnn codes read as Windows-1252

* Integration: #2416 collapse the nested vertical-align if (clippy::collapsible_if)

* Integration: #2185 hyperlink_target slices with get() and needs the whole field name

* Integration: #2034 objects without a stacking order draw above the ones before them

* Integration: #3434 drop review-bot notes from test doc comments

* Integration: #3419 doc comments back on their own items (EquationFrame and its test above them)

* Integration: #2334 test a formula string result continued in a CONTINUE record

* Integration: #3429 the based-on-chain test's doc comment back above its test

* Integration: #2453 drawing_payload test builds the shape fill as a Fill

* Integration: #3257 separate its Find-box test from #2407's recent-files test (auto-merge interleaved them)

* Integration: cargo fmt

* Integration: #1728 close the layout test the merge resolution left open

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: cargo fmt

* Integration: #2722 Dutch registry comment on its own line

* Integration: #3203 English text for the keys the Romanian catalog lacks

Its complete-catalog test needs every key of every other catalog; the merged PRs added 72 since it was written. Rows carry the English text until translated.

* Integration: #1076 English text for the keys the Slovenian catalog lacks

Its complete-catalog test needs every key of every other catalog. Rows carry the English text until translated.

* Integration: status lines list each change once

Combining the tracks' status lines repeated a few entries (#2852, #1500, #2617).

* Integration: cargo xtask parity

* Integration: cargo fmt

* Integration: #1832 drop a useless .into() in the end-of-document citation test

clippy::useless_conversion failed cargo xtask ci.

* Integration: #2244 VML on/off values read case-insensitively by the shape fill reader

Main (#1500) moved VML fill reading into shape_style.rs, where filled="False" still counted as on; #2244's test vml_shape_wrap_side_and_anchor_case_insensitivity expects no fill.

* Integration: #2041 English text in the Polish catalog for the keys Italian gained

polish_covers_every_key_of_every_other_catalog expects Italian to have only "Style: APA" beyond Polish; #2041 translated 14 newer strings into Italian. Rows carry the English text until translated.

* Integration: #2722 English text for four labels the Dutch and Danish catalogs lack

every_tab_and_command_is_translated needs Format Shape Pane, Full Screen View, Personal Templates and Underline Color, added by other merged PRs. Rows carry the English text until translated.

* Integration: #3335 header Esc test allows the half-pixel return scroll that #3201 brings

#3201 makes entering an empty header scroll to it; Esc then scrolls back to the caret, landing 0.5 px from the start (17291.0 vs 17290.5). The test checks the view is back within a pixel.

* IME: the caret follows the composition text (#3556)

* Header and footer: a click in the other band edits it there (#3557)

* README: the app icon above the title

* Release 0.10.0

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

* The inspector's removals are refused in every protected mode

`file.inspect` with removals takes content out of the document — the
comments, the revisions, the properties, the hidden text, the headers.
Under readOnly and comments it was refused, or rolled back by the
post-check where the document had an exception; under trackedChanges
nothing stopped it, so a document that tracks changes could have its
comments and revisions taken out with no revision to show for it.

The removals are refused now, in every mode a protected document can be
in, next to the properties and the privacy options. An inspection without
removals only reads the document, and still works.

The protection test covers the modes now, not only readOnly.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the session author, not the user name

"the user name can't change while the document is protected" was the
wrong term twice over: it is the session author — the name new tracked
changes and comments are recorded under, and the one a named exception
is matched against — and it is `file.setAuthor` that refuses to change
it. The CLI's `--author` names the session directly, so it works on a
protected document.

The parity row, the two status lines and the two progress rows now say
that.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3279: Keep protected session author fixed in CLI and Options

* The Document Inspector refuses its removals while the document tracks changes

The inspector's removals delete straight from the document: the hidden
text, the comments, the revisions and the headers go without ever reaching
the engine's tracked-delete path. On a document that tracks changes — and
`trackedChanges` protection turns tracking on — the content was gone with
no revision to show for it, and the revision trail was broken behind the
user's back.

The removals are not tracked, so they cannot be: rather than untrack them
silently, the inspector refuses the whole removal form while the document
tracks changes, and says why ("the document tracks changes: stop tracking
before removing content"). The check comes before anything is removed, so
the document is left untouched, and reading the document — the inspector
without removals — keeps working. A document that does not track changes
is inspected as before.

Co-Authored-By: opencode <noreply@opencode.ai>

* The inspector's tracking refusal doesn't cite an unrelated issue

#1344 is about the inspector missing hidden text, not about its removals
going untracked, so the reference only sent a reader to the wrong place.

Co-Authored-By: opencode <noreply@opencode.ai>

* A version snapshot belongs to the document it was taken from

`Session::set_document` resets everything about the open document — the
document id, the document, the selection, the undo history, the password,
the file name — but not the version snapshots. A snapshot taken of one
document therefore survived `file.new`, `file.open`, `file.newFromTemplate`
and `file.recover`, and `file.versions {"restore": index}` then put the
previous document's content into the open one.

The snapshots go with the document they were taken from: `set_document`
clears them, so a restore can only ever bring back a version of the
document that is open. Every caller replaces the document with another
one (a new or opened file, a recovered copy, a merge result, a sheet of
labels), so none of them loses anything it needs.

Co-Authored-By: opencode <noreply@opencode.ai>

* Docs: the inspector's tracking refusal and the version snapshot's scope

Co-Authored-By: opencode <noreply@opencode.ai>

* A comparison clears the version snapshots of the document it replaces

`review.compare` and `review.combine` open the original with `s.doc = doc`,
which bypasses `set_document` — so the version snapshots of the document the
comparison replaces survived it, and a restore could put the previous document
back over the comparison result.

The snapshots go with the document they were taken from, as everywhere else.
The document itself is still assigned directly: `set_document` resets the undo
history, and the comparison's own undo step would go with it.

Co-Authored-By: opencode <noreply@opencode.ai>

* #3277: Test Inspector refusal without protection

* #1327: Keep snapshots with their document through Compare

* #1327: Preserve file identity after rejected document replacement

* test: mark the portal test's blocking sleep as intentional (SonarCloud) (#3989)

The future is polled by hand with no async runtime, so a blocking sleep
is how the test models a slow dialog.

🤖 Generated with Codebuff

Co-authored-by: Codebuff <noreply@codebuff.com>

* #3542: gate the label_of test off wasm32

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

---------

Co-authored-by: Divyanshu Gupta <divyanshu2074@gmail.com>
Co-authored-by: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: vangel76 <vangel1976@googlemail.com>
Co-authored-by: aiongg <aiongtaigi@gmail.com>
Co-authored-by: aiongg <67543387+aiongg@users.noreply.github.com>
Co-authored-by: bflatastic <bombayapirat@gmail.com>
Co-authored-by: Brandon Thomas <brandon@storyteller.ai>
Co-authored-by: Mat Strange <mat@matstrange.net>
Co-authored-by: opencode <noreply@opencode.ai>
Co-authored-by: skylar taylor-barrick <hi@skylar.cc>
Co-authored-by: Dylan M. Taylor <dylan@dylanmtaylor.com>
Co-authored-by: Codebuff <noreply@codebuff.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-23 10:25:42 -04:00
aiongg 08028c44c0 Engine: several open documents; file.new opens another
One session holds several open documents. The active document stays in
the Session's own fields, so commands and the UI keep working on it; the
others are parked as DocState values and swapped in field by field
(std::mem::swap, no clones or relayout). Session and DocState are
destructured exhaustively, so a new field must be classified as per
document or shared before it builds.

- Session::documents, activate, add_document, close_document,
  is_untouched_blank, with_document, find_open, is_open; DocInfo.
- Document ids come from a process-wide counter.
- file.new opens beside the open documents; file.open replaces an
  untouched blank document, opens beside the others otherwise, and
  activates a file that is already open ("alreadyOpen": true).
- file.closeDocument {id?, force?} closes a document without a window
  (refuses unsaved changes unless forced); file.close is unchanged.
- view.switchWindows {id?} lists the open documents or activates one.
- Envelopes, Labels, Finish & Merge and New from Template open their
  result as a new document; the main document stays open, with no undo
  step for the command.
- Each document keeps its own recovery worker and copy; Read Aloud
  readings name their document; with_document tags UI requests with the
  document they came from.
2026-10-22 14:41:20 +00:00
3a2cea6ca4 #1843: Read Aloud speaks Russian text with a Russian voice (#2430)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-18 04:55:18 -04:00
60b9500347 Web: add option to load system installed fonts in dropdown (#1390)
* feat(web): add option to load system installed fonts in dropdown

Add explicit 'Load System Installed Fonts…' button in font dropdowns for web (Wasm).

- Dispatches font.loadSystem via transient user activation on click
- Calls window.queryLocalFonts() to enumerate and load installed fonts into FontDb
- Invalidates font family cache in FontPreviews upon new font installation
- Complete localization across all 18 languages
- Credit GhostMitra in contributors/people.toml

* fix(ui, engine): canvas focus after chrome click and checked file access in sniff

* Revert "fix(ui, engine): canvas focus after chrome click and checked file access in sniff"

This reverts commit b0c925bf79.

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 23:24:19 -04:00
04f6f53b8a Read Aloud starts PowerShell without a console window (#1122) (#1150)
* #1122: Read Aloud starts PowerShell without a console window

On Windows the speech program is PowerShell, a console program: each sentence it spoke flashed a console window over the document. It now starts with CREATE_NO_WINDOW.

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

* #1122: the console-window test checks the flags Read Aloud passes

windows_speech builds the PowerShell program, arguments and creation flags
that speak() turns into its Command; std can't read creation flags back from
a Command, so the test checks that value (CREATE_NO_WINDOW) and the arguments.

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

---------

Co-authored-by: Dmitrii Khlapov <19991457+dkhlapov@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 02:05:18 -04:00
65c9b2e5ec speech: launch Windows PowerShell by full path for Read Aloud (#792)
Read Aloud on Windows started `powershell` by name, so the program search
decided which executable ran. Start
%SystemRoot%\System32\WindowsPowerShell\v1.0\powershell.exe instead
(C:\Windows when SystemRoot is unset), as PdfCraft already does for
rundll32. The command line and stdin handling are unchanged.

Adds speech::tests::powershell_is_launched_from_the_system_folder.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-12 01:16:07 -04:00
fe7e79ad70 Read Aloud: player with speed control, sentence skipping, skips citations (#190)
* Zotero 1: range fields and custom document properties round-trip

Citation managers' ADDIN fields (Zotero, Mendeley, EndNote) are now kept as
range fields: FieldStart/FieldEnd markers with the result as ordinary,
formatted content between them, possibly spanning paragraphs (a Zotero
bibliography). Before, the result was flattened to plain text and a
multi-paragraph field lost its end.

- doc: InlineObject::FieldStart/FieldEnd, Document::field_ranges,
  balance_field_ranges; CustomProp and custom_props with get/set/remove.
- docx: read ADDIN fields as ranges (field codes up to 2 MiB), write them
  back as begin/instr/separate…end; orphan markers are dropped on save so
  files always hold whole fields. docProps/custom.xml is parsed and
  regenerated (simple types as text, vectors/blobs verbatim) instead of
  passed through, so Zotero's ZOTERO_PREF_n settings are editable.
- engine: file.properties reads and sets custom properties.

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

* Zotero 2: wire-protocol client and document bridge

New crate wordcraft-zotero (L5) speaks Zotero's published word-processor
protocol (port 23116; 32-bit transaction id + big-endian length frames,
JSON payloads, ERR: replies):

- client: sends an integration command (addEditCitation,
  addEditBibliography, addNote, refresh, removeCodes, setDocPrefs) and
  answers Zotero's calls until Document_complete or the connection
  closes (cancelled dialog). Answers go through a callback so the app can
  apply them on its UI thread; run_on_session answers directly.
- Bridge: implements Zotero's Document_*/Field_* calls on a Session.
  Fields are stored as Word's plugin stores them (ADDIN ZOTERO_ range
  fields; prefs in ZOTERO_PREF_n, with Zotero's ReferenceMark field type
  kept as Word's Field in the file), so documents stay Word-compatible.
  Rich text (RTF or HTML) keeps citation formatting; bibliographies get
  a Bibliography style from setBibliographyStyle; note styles create
  footnotes/endnotes, and deleting a lone note citation removes its note;
  placeholder links convert to fields. One transaction = one undo step.
  Field ids stay stable through inserts and deletes.
- Tests: a fake Zotero over a real socket, every call, docx round trip,
  hostile arguments for every method, random frames (proptest).

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

* Zotero 3: Zotero in the app and the CLI, tested with the real Zotero

- ui.zotero.<command> (addEditCitation, addEditBibliography, addNote,
  refresh, removeCodes, setDocPrefs), ui.zotero.status and
  ui.zotero.answer. The socket runs on a background thread; each of
  Zotero's calls is answered on the UI thread against the session, and
  Zotero's questions become a dialog the session waits on. Web builds
  report that Zotero needs the desktop app.
- wordcraft-cli zotero <command> <file> [--save] [--trace] [--port].
- Bridge: a selection Zotero makes to show a citation is put back when
  the session ends.
- ui_shot: {"wait": ms} keeps frames running for background work.
- docs/zotero.md.

Checked against Zotero 10.0.6: refresh of a citation and bibliography
(APA), its "modified citation" question answered in the app's dialog,
RTF results with italics, Bibliography style, prefs round trip.

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

* Zotero 4: switching between in-text and note styles moves citations

Document_convert and Field_convert now move a citation between the
text, footnotes and endnotes, as Zotero asks when the document's style
changes between an in-text and a note style: into a new note at the
same place, or back into the text where the note mark was (removing a
note that held only the citation; keeping one with other text). Field
ids survive the move, formatting is kept, and each Zotero session stays
one undo step.

Checked against Zotero 10.0.6: unlink citations (removeCodes, with its
confirmation question) and a refresh after switching the document to
Chicago notes.

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

* Zotero 5: Zotero tab in the ribbon

A Zotero tab (before Help) with Add/Edit Citation, Add Note, Add/Edit
Bibliography, Refresh, Document Preferences and Unlink Citations. The
buttons are disabled while a Zotero command is running and in the web
build. Three new icons drawn in code (document preferences, unlink,
add note); the others reuse the References tab's. Labels and status
messages translated (zh-hans, zh-hant, ja).

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

* Zotero: Add/Edit Bibliography inserted nothing

Zotero reads a newly inserted bibliography field back before filling
it, and unlinks a field whose text is empty, taking it for a
bibliography the user deleted. Our new fields were empty, so Add/Edit
Bibliography ended with Field_removeCode and nothing in the document.
New fields now hold "{Citation}" until Zotero writes the result.

Found by tracing the session with Zotero 10.0.6; regression test added.
wordcraft-cli zotero takes --cmd (run before the Zotero command, e.g.
caret.docEnd) to reproduce sessions from a given caret position.

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

* Zotero: log each command's outcome and Zotero's calls to the app log

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

* Read Aloud player: speed, previous/next sentence, skips citations

Read Aloud now speaks sentence by sentence with the system speech program
(say / espeak / Windows SAPI) on a worker thread, so it can pause, skip
and change speed. A floating player window has previous / play-pause /
next / stop, a 0.5-3x speed slider and "Skip citations & bibliography"
(on by default: Zotero, Mendeley, EndNote and Word citation fields).
The spoken sentence is highlighted and the caret follows it.

New commands: readAloud.playPause/next/previous/stop/speed/
skipCitations/status, and caret.pastCitation (Zotero > Move Past
Citation). Speed and skip preference persist with the UI prefs.

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

* Cargo.lock: wordcraft-zotero at the workspace version (0.4.0)

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

* Read Aloud: no unused BASE_WPM warning on wasm

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

* Read Aloud: fuzz review.readAloud in hostile_params_never_panic

#190 makes the engine's speech backend silent (Hold) under cfg(test), so the
#75 skip is no longer needed; assert the backend instead so a test can
never start the system synthesiser.

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
2026-10-10 08:36:19 -04:00