Skip to content

PDF Tools v2 follow-ups: local comment dates, Edit alignment and colour, real storage line, kept covers, Combine's footer - #479

Merged
ymote merged 18 commits into
mainfrom
feat/pdftools-v2-followups
Oct 11, 2026
Merged

ymote merged 18 commits into
mainfrom
feat/pdftools-v2-followups

Conversation

@ymote

@ymote ymote commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

The approved follow-ups to PDF Tools v2 (#473, on the pdf service v2 of #472), the two changes the user asked for after the demo (one search, Read mode), and the review's fixes. Rebased on main bcd52cf (#484).

What changes

  1. Comment dates (bug). The engine stamps its own marks in UTC (/M (D:20261011003256Z)). Its comment_list passes /M through pdfcraft-render's pretty_date, which drops the zone. So a mark made at 17:32 PDT on 10 Oct was listed as 2026-10-11T00:32, labelled local.
    • pdf.comments now reads each comment's and reply's /M as the file writes it, through a reviewed read-only route. That route is the engine's own comment reader (pdfcraft_annot::summaries, the reader its listing comes from), run over a pdfcraft_cos document opened on the open document's working bytes: no file, no path, no script, one pass.
    • It converts the date from its zone (Z or ±hh'mm') to this device's local time. The offset comes from where the shell's clock reads it: localtime_r's tm_gmtoff on Unix, chrono's Local elsewhere, at the date's own instant, injectable in tests.
    • A date with no zone is taken as written.
    • The dates are read once per version of the document: the cache follows the engine's working bytes, which every edit, undo, redo and save replaces (review M2).
    • The two engine crates join the root pins at the same pdfcraft rev.
  2. Edit: alignment and colour. pdfcraft's text_edit (at the pinned d801407a) formats a paragraph with both.
    • pdf.edit_text takes optional align (left, center, right, justify) and color (#rrggbb), with paragraph only.
    • The service writes the colour it hands the engine from the bytes it parsed, never an engine colour name, and refuses anything else.
    • Font, size, spacing and move parameters stay excluded. The review is beside the method.
  3. Storage line. Splash has no storage-usage call, so the shell's files.status now reports used_bytes and quota_bytes.
    • used_bytes is the host's measure of the app's jail, the one the engines' room checks use.
    • quota_bytes is the live isolate's quota.
    • The README and the host-API status page are updated in English and Chinese.
  4. Covers kept across close (pdf.cover {path, dpi?}, approved on 10 Oct).
    • Key: page 1 goes to .cache/covers/<name-hash>-<size>-<mtime>@<dpi>.png. Older covers of the same file are removed by an exact name-hash prefix.
    • Cache: a kept cover answers from its metadata with no engine work. A new one is a fresh session with JavaScript off, with no open document and nothing counted toward the 8.
    • Caps: covers live inside the render cache's 16 MiB / 64 files, at most 32 of them. Page renders go first, and the whole cache is cleared before storage_full:.
    • pdf.close, the isolate listener and prune reach .cache/pages only.
  5. SERVICE.md: follow-ups, its own commit: the date semantics, align/color, the pdf.cover row with caps and errors, and the storage ground rule.
  6. App (main.splash):
    • Dates now read as local time.
    • Design 08's Alignment: the paragraph's own alignment is preselected, read from its lines' boxes, because the engine sets an edited paragraph flush left unless told.
    • Colour: a box that opens into the paragraph's own colour and six others. Nothing is sent until one is picked.
    • The storage line comes from files.status, refreshed after writes.
    • Covers: see the review's fixes below. Remove deletes a PDF's cover.
    • Combine (Open a script app at its manifest's window size; PDF Tools asks for 1536 x 1024 #478 follow-up): when the list doesn't fit the window, the card fills the pane, its footer stays in view, and the list scrolls inside it, in a static scroll view that keeps its place. When it fits, the card is as design 06 draws it.
    • The dev fixture answers the new fields. Model tests cover the new pure functions (19 in all).
  7. One search, in the top bar (the user: "side search and top search are duplicated"):
    • The rail is Pages, Outline and Comments. The top bar's Search in document opens its results in the left panel, with no rail item lit.
    • Done, the results' own cross and the field's cross all end the search. Each gives the left side back as it was before the search, or Pages if that is unknown. Before, the results' cross only folded the panel and left the search running.
    • A change to the document ends the search the same way.
  8. Read mode, the pages alone (the user: "there is no full screen read mode?"):
    • A full-screen button ends the floating bar. Read mode hides the tabs, the mode bar, the rail and its panels, the tool row and the status line.
    • The bar keeps page navigation ("3 / 24"), zoom, fit width and fit page, and "Exit read mode". An exit control sits in the top right corner.
    • The page and the zoom are kept both ways. Page taps do nothing in Read mode, and marks and editors are hidden.
    • It fills the app's window, not the screen: no host API lets an app make the OctoSense window full screen. Both READMEs say so.
  9. The review's fixes, each in its own commit:
    • M1: a save records the document's page count in the library and marks its cover stale. So Home and Combine no longer show the old count after a save and a restart.
    • M2: comment dates are read once per version of the document (above).
    • L1: pdf.cover's too_large is handled like storage_full: no cover, with pages and size from pdf.info. It no longer marks the PDF as unreadable.
    • L2: Home asks pdf.cover for each PDF once a start. A cover at a new path means the PDF changed, so its pages and size are read again.
    • L3: covers for the first 32 PDFs only, as many as the service keeps. The rest get pages and size.
    • L4: ui.py's restart run checks that Remove deletes the PDF's cover. A test opens a hostile multi-byte /M safely. apps/AGENTS.md describes the covers, and the Chinese files README's files.status row matches the English one.

Verified (macOS, Apple silicon)

Before the rebase onto #484: head 12e0e4cb on main 07b63da. The shell for the runs was built from 94b6f044, whose tree differs from 12e0e4cb only in ui.py.

  • cargo test --locked -p octosense-pdf-service: 56 unit tests and the first_call and skill tests. Engine crate tests aren't in CI (CI never runs the twelve engine services' own tests (including their containment tests) #412).
    • Dates: a Z, a +02'00', a -04'00' and a zone-less date for comments and replies under fixed offsets, plus a mark made now, saved and reopened.
    • The dates are read again only after an edit, undo, redo or save, and a hostile multi-byte /M is read safely.
    • Alignment and colour as the engine draws them, and every refusal.
    • Covers: kept across close, prune, release and a new run (unreadable bytes with the same size and time still answer from the cache), a saved file's new key, the 32-cover cap, pages evicted first, cleared before storage_full:, no open-document entry.
    • A cover runs no script: an XFA form's initialize script sets qty with JavaScript on and the drawing differs, while the cover equals the scripts-off render.
    • Refusals, and a link at .cache/covers.
  • cargo clippy --locked -p octosense-pdf-service --all-targets --no-deps -- -D warnings: clean.
  • From phone/, cargo test --locked --features mobile-apps -p octosense-shell: 1,185 passed.
    • It includes files_service: a child process seats a live isolate on real host storage and checks used_bytes and quota_bytes.
    • It includes the 19 pdftools_model tests.
  • python3 tools/setup.py --check --cargo, python3 tools/native_apps.py --check, check-shell-graph.sh for octosense and octosense-home, and rom/tests/test_no_local_paths.py: pass. The desktop builds (app-hub,craft-engines).
  • hub check --system-app: the same two findings as main's bundle (no store listing, unsigned). Nothing new.
  • apps/pdftools/tests/ui.py in a hidden desktop shell with the real engine, at PDF Tools' own window size (an app area of 1292 x 662). The shell (light then dark), restart, full and empty runs all passed with --budget-ms 1000 (see the next item). Evidence and receipt are in apps/pdftools/tests/evidence/v2-followups-20261010/.
    • Read mode: entered on page 2 of 4; a page turned and the zoom moved from 100% to 125%. It was left by Exit read mode (light) and by the corner control (dark), each time on page 3 at 125%.
    • One search: the rail read Pages, Outline, Comments, and the results took the left panel. Done gave the Comments panel back; the results' cross folded the left side again.
    • Marks read "Today 00:34" at 07:34 UTC.
    • The status line read "187 KB of 64 MB used" for a storage of 191,793 bytes. The PDFs alone are 38,356, which is all the old line counted.
    • The engine drew the edited paragraph centred and red, then flush right.
    • Combine's own button stayed at 613–659 of the 662-point area with two PDFs. Before, it was 85 points below the fold.
    • The restart drew none of the 5 kept covers again. Remove then deleted the removed PDF's cover.
    • With the storage full, the cards still show pages and size.
  • The runtime's 64 ms budget. At 64 ms, the shell run passed every functional step, light and dark, in two attempts. Each then failed ui.py's log check on script budget overruns, so restart, full and empty didn't run.
    • Each overrun came with a UI-thread stall in a pdf service file rename: 555 ms in pdf.save, then 262 ms in the render cache. App Hub runs host services inline on the UI thread (Engine host services run inline on the UI thread under the SERVICES lock #399), and this PR doesn't change those paths.
    • The one-minute load average was 54 to 74 then: six cargo builds and tests of other sessions, the malware scanner at 130 to 200% CPU, and the media analysis daemon.
    • run-64ms/ keeps the third attempt's logs, and the second attempt's error and hang lines, read before the third overwrote its log.
    • On 10 Oct, at load 15 to 35 and before one search and Read mode, all four runs passed at 64 ms.
    • The first attempt at 64 ms stopped at a ui.py bug in the new rail check. The first at 1000 ms lost a tap on Combine's Add files in the dark pass, and ui.py now taps again while the list hasn't shown.
  • App Hub's card-host (fixture run) at the designs' 1536 x 1024, light and dark: passed. That includes the find screen beside design 03, whose rail still draws a Search item.
    • The card-host on this machine (App Hub 8347a489) predates the window hint, so the scratch copy's manifest dropped window/schema_minor. The script is the branch's own.

After the rebase onto bcd52cf, which brought in #484 alone (App Hub's pin moved from d86101d7 to c317f787): python3 tools/setup.py --check --cargo, python3 tools/native_apps.py --check and the pdf service's tests (56 + 1 + 1) pass.

Not verified

  • After the rebase onto Support WASI TCP, UDP and DNS with bounded cancellable resources #484: the hidden-shell runs, the shell suite and the desktop build.
  • restart, full and empty at the runtime's 64 ms in this round.
  • Linux and Windows. Windows's offset path (chrono Local) wasn't built here. Phones: the pdf engine is desktop-only.
  • card-host at the current App Hub pin.
  • The local CI gate (tools/ci-local.sh).

Notes

  • Engine finding, not fixed here: pdfcraft-render's pretty_date slices d[..12] before checking the bytes are digits. A crafted /M whose 12th byte falls inside a multi-byte character (/M (D:12345678901\351)) panics in inspection (inspect.rs:958, checked locally). The engine catches it, but the otherwise valid PDF then fails to open with damaged: … the document has no pages. The service's own date code checks the bytes first.
  • The engine reports neither a paragraph's alignment nor its colour. The panel reads alignment from the line boxes and shows "Its own colour" until a colour is picked.

🤖 Generated with Claude Code

@ymote
ymote force-pushed the feat/pdftools-v2-followups branch from 3fec286 to 358afa8 Compare October 11, 2026 07:59
ymote added a commit that referenced this pull request Oct 11, 2026
The user, from the live demo of #479: "side search and top search are
duplicated". Search in document in the top bar is now the one way to
search. The left rail keeps Pages, Outline and Comments, and has no Search.

- Typing in the top field and pressing Return still opens the results in
  the left panel: "N matches on M pages", the list, previous and next, Done.
  No rail item is lit while they show.
- A search remembers what the left side showed before its results took it
  (a panel, or the left side folded). Done, the results' cross (which only
  folded the panel before) or the field's cross ends the search and gives
  that back, Pages when it is unknown. It does so now, or when Comment or
  Edit gives the left side back.
- A change the engine makes, which drops the stale matches, ends the search
  the same way. Before, it left an empty results panel that only the rail's
  Search led back to.
- Nothing else opened the results: there were no shortcut buttons for them,
  and no find state survives a restart (library.json keeps the page and
  zoom, not the panel).

Model tests cover the rail's items, which item is lit while results show,
and the left side a search gives back. ui.py's shell journey checks the
rail. Its light pass shows Done giving back the Comments panel. Its dark
pass shows the results' cross folding the left side again. The README notes
in English and Chinese that ⌘F's stand-in is the field itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ymote added a commit that referenced this pull request Oct 11, 2026
Review of #479 (M2): pdf.comments read the whole document again on every
call, on the UI thread (#399). Typical documents took 1 to 8 ms. It
measured about 90 ms at 20k pages, about 230 ms with 100k comments, and
about 100 ms for a 115 MiB file with a broken xref, which is rebuilt on
every call.

The dates read now stay on the open document, with a weak reference to the
working bytes they came from. The engine swaps those bytes for new ones on
every edit, undo, redo and save. The weak reference keeps their address from
being reused, without keeping the bytes, so a kept read matches only the
version it came from: right by construction. A second pdf.comments with no
change reads nothing; the next one after a change reads once.

Tests:
- a second call reads nothing, and an edit, an undo, a redo and a save
  each read once (a test-only counter);
- hostile dates (é where a date's digits or its zone go, and alone) are
  read without a panic: a zone that is not one is no zone, and text that is
  no date stays as written (review L4).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ymote added a commit that referenced this pull request Oct 11, 2026
Review of #479 (M1, L1–L4). The library learned a PDF's pages once and
never again. Before #479 every start reopened each PDF; with kept covers,
nothing refreshed them. So a 4-page PDF with a page deleted and saved said
"4 pages" on Home after a restart, and Combine said "All (4 pages)". A
range of "4" then passed the app's check, and the merge was refused.

- A save records the open document's page count in the library, and marks
  the cover stale.
- Once a start, Home asks pdf.cover for each PDF it shows. A kept cover
  answers from its file alone (a stat and a 24-byte read). A cover at a new
  path means the PDF changed, so its pages and size are read again from
  pdf.info (L2).
- pdf.cover's too_large (a first page over 16 megapixels even at 48 dpi) no
  longer marks the PDF "Couldn't open this PDF" and drops it from Combine's
  and Insert's pickers. Like storage_full: no cover, pages and size from
  pdf.info (L1).
- Home asks covers only for the first 32 PDFs it shows, as many as the
  service keeps. Asking for more evicted the first ones every start, so every
  start re-rendered everything. PDFs beyond those get their pages and size
  only (L3).

ui.py's restart run now removes a PDF from Home and checks that its kept
cover goes with it (L4). Before, the full run, which removes a PDF, had no
covers to check. Both READMEs describe the covers as they are now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
ymote added a commit that referenced this pull request Oct 11, 2026
…nese

Review of #479 (L4):
- apps/AGENTS.md described the pdf service's render cache as .cache/pages/
  alone. It now names pdf.cover, .cache/covers/, the 32-cover cap within the
  16 MiB / 64-file cache, page renders going first, and comment dates read
  once per version of the document.
- The Chinese files README's files.status row now lists the three older
  fields its English row has: photo_pick_supported, text_share_supported
  and max_share_text_bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ymote
ymote force-pushed the feat/pdftools-v2-followups branch from 358afa8 to 4806eb5 Compare October 11, 2026 08:00
ymote and others added 18 commits October 11, 2026 06:52
The engine lists a comment's /M through pdfcraft-render's pretty_date,
which keeps the file's wall clock and drops the zone, and it stamps its
own marks in UTC: a mark made at 17:32 on 10 Oct in California came back
as 2026-10-11T00:32, labelled local, and PDF Tools showed "11 Oct".

pdf.comments now reads each comment's and reply's /M as the file writes
it, with the engine's own comment reader (pdfcraft-annot's summaries, the
reader its listing comes from) over a pdfcraft-cos document opened on the
open document's working bytes, and takes it to this device's local time
from its zone (Z or +hh'mm'). The route only reads: the engine's copy of
the bytes in memory, no file, no path, no script, one pass. A date with no
zone is taken as written, as is the listed wall clock when the file's own
date cannot be read or is not the one listed.

The offset comes from where the shell's clock reads it (localtime_r's
tm_gmtoff on Unix, chrono's Local elsewhere), at the date's own instant,
and tests fix it. SERVICE.md's contract does not change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Design 08's Edit panel has Alignment (left, centre, right, justify) and
Colour. pdfcraft's text_edit (at the pinned d801407a) formats a paragraph
with both: align offsets each rewrapped line, and justify uses word
spacing for every line but the last; color fills it in its own graphics
state.

pdf.edit_text takes optional align (left, center, right, justify) and
color (#rrggbb, either case), with paragraph only. The service writes the
colour it hands the engine from the three bytes it parsed, never one of the
engine's colour names, and refuses anything else before the engine sees
the call. The font, size, underline, spacing and move parameters stay
excluded. The review is beside the method (src/change.rs).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PDF Tools' Home rendered every cover again on each start: it opened each
PDF, rendered page 1 and closed it, and closing deletes a document's
renders, as a later open prunes them.

pdf.cover {path, dpi?} draws page 1 of a PDF in storage into the render
cache's .cache/covers/<name>-<size>-<modified>@<dpi>.png, all lowercase hex
and digits: <name> hashes the storage-relative name, so a PDF's covers
share an exact prefix no other PDF's carry, and a saved or replaced PDF
gets a new key while its older covers go. A kept cover answers from its
metadata and PNG header, with no engine work; a new one is a fresh
session with JavaScript off, like pdf.info, with no open document, no
handle and nothing counted toward the 8. Close, the isolate listener and
prune reach .cache/pages only.

Covers share the render cache's 16 MiB and 64 files, at most 32 of them.
Page renders go before covers when it needs room, and the whole cache is
cleared before a write would fail with storage_full:. dpi is 24 to 150
(default 48), at most 16 megapixels. The coordinator approved the
contract on 10 Oct 2026.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The contract changes decided for PDF Tools v2's follow-ups:

- Comments: a comment's and a reply's date is this device's local time,
  converted from the zone the file writes it with (Z or +-hh'mm'); a date
  written without a zone is taken as written.
- Edit: pdf.edit_text takes align (left, center, right, justify) and color
  (#rrggbb), which format a paragraph only. It notes what the engine does
  without them.
- Reading: pdf.cover {path, dpi?}, a PDF's first page kept across closes in
  the render cache's .cache/covers, with its key, caps and errors. The
  render cache's caps now count covers.
- Ground rules: the status line's storage use comes from the shell's
  files.status (used_bytes, quota_bytes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
PDF Tools' status line should read "14.6 of 64 MB used" from the real
numbers. It counted only its library's PDFs, against a constant. Splash has
no storage-usage call (makepad's fs module: read, write, exists, remove,
mkdir, list), and keeps the quota host-side.

With live storage, files.status now also reports:

- used_bytes: the host's measure of the app's jail, the root App Hub gives
  its isolates (app_storage::Storage::usage, which the engines' room
  checks use);
- quota_bytes: the calling isolate's storage quota, the one its own writes
  and imports are checked against.

Both are left out when either is unknown. Status still opens no dialog and
needs no foreground. The discovery schema declares both. The README and
the host-API status page say so in English and Chinese.

Tests:
- a child process seats a live isolate on the host's app storage: the
  jail's bytes, a write showing on the next status, the quota;
- without live storage, neither is measured.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rage, kept covers

The app half of the v2 follow-ups, on the pdf service's new methods:

- Comment dates: the service now gives this device's local time, which the
  thread cards already read as such ("Today 19:42", not "11 Oct" for a
  mark made after 17:00 in California).
- Edit (design 08): Alignment, four segments (left, centre, right,
  justify), with the one the paragraph's lines show chosen. The engine
  sets an edited paragraph flush left unless told, so an edit always
  names it. Colour is a box that opens a row of six colours; nothing is
  sent until one is picked, since the engine does not report a
  paragraph's colour. Apply sends pdf.edit_text {align, color?}; an
  alignment or a colour alone is a change.
- Storage line: what the app's storage holds against what it may hold, from
  files.status (used_bytes, quota_bytes), asked again a moment after each
  burst of writes. A host that does not say falls back to the library's
  PDFs against the system ceiling.
- Covers: Home asks pdf.cover for a PDF without a kept cover, and pdf.info
  once for its pages and size, all kept in library.json. A start with
  every cover kept asks the engine nothing. A save marks the PDF's cover
  stale; Remove deletes it.

Also:
- The dev fixture answers pdf.cover and refuses edit_text formats as the
  service does.
- Model tests cover the storage words, the alignment read from line boxes,
  and what an edit sends.
- Both READMEs drop the two known issues these fix.

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

With the desktop's window above the dock (1292 x 662, #478), Combine's own
button sat 85 points below the fold with two PDFs listed. Now:

- When the list fits, the card is as design 06 draws it.
- When it doesn't, a tall card fills the pane. Its title and footer ("One
  PDF of N pages", Name and the Combine button) stay in view, and the file
  list scrolls inside it.
- The tall card's list is a static scroll view, so it keeps its place
  while the list is drawn again (adding a PDF, typing its pages).
- combine_list_h and combine_fits do the height math from the design's
  measures, and are model-tested.

In the shell's default window (990 x 603):
- The Edit panel draws its gaps tighter, so Apply and Cancel stay in view
  under Alignment and Colour.
- Colour's box opens into its seven colours in its own row instead of a
  row below it.

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

ui.py's shell runs now also check what the follow-ups changed, with the
real engine:

- the status line against the storage's own bytes (files.status);
- the new highlight and its reply dated "Today HH:MM" on this machine's
  clock (the engine stamps them in UTC);
- the Edit panel's Alignment and Colour set before Apply (centred and red,
  then flush right in the paragraph's own colour);
- Combine's own button in view with no scroll, with the list free to scroll
  inside the card;
- one cover per PDF in the cover cache, each named in library.json, and
  every current cover kept untouched by a restart.

What the runs saw goes to <output>/run.json for the receipt.

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

With the storage full, pdf.cover answers storage_full. Home then asked
nothing more, and the cards said "Reading…" for good. The old pipeline read
a PDF's info before its render failed. A cover refused for room, or by a
shell without pdf.cover, now still fetches the pages and size from pdf.info,
which writes nothing. A damaged or locked PDF still says so on its card.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Combine finds a PDF's page field in that PDF's own row, once the row sits
  still: a list scrolled by the wheel can still be moving, and a click could
  land on the row beside it.
- The full-storage run waits until no card says "Reading…": each card shows
  its pages and size though there is no room for its cover.
- The Edit step grabs the Colour box opened into its colours (08a-colours).

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

ui.py's shell, restart, full and empty runs, rebased on main with #478.
They ran in a hidden desktop shell with the real pdf engine, at the runtime's
64 ms budget, with PDF Tools at its own window size (an app area of 1292 x
662), light and dark. The fixture run used card-host at the designs'
1536 x 1024.

What they show:
- comment dates in this machine's local time ("Today 20:21" when UTC was
  03:21 the next day);
- the Edit panel's Alignment and Colour applied by the engine;
- the status line from files.status ("187 KB of 64 MB used" for a storage
  of 191,793 bytes; the PDFs alone are 38,356);
- one cover per PDF, kept untouched across a restart;
- Combine's own button in view with two PDFs listed (before, 85 points
  below the fold);
- with the storage full, cards that still show their pages and size.

The receipt holds the digests, the commands and the numbers. The README's
Verified, Not verified and Known say so in English and Chinese.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The user, from the live demo of #479: "side search and top search are
duplicated". Search in document in the top bar is now the one way to
search. The left rail keeps Pages, Outline and Comments, and has no Search.

- Typing in the top field and pressing Return still opens the results in
  the left panel: "N matches on M pages", the list, previous and next, Done.
  No rail item is lit while they show.
- A search remembers what the left side showed before its results took it
  (a panel, or the left side folded). Done, the results' cross (which only
  folded the panel before) or the field's cross ends the search and gives
  that back, Pages when it is unknown. It does so now, or when Comment or
  Edit gives the left side back.
- A change the engine makes, which drops the stale matches, ends the search
  the same way. Before, it left an empty results panel that only the rail's
  Search led back to.
- Nothing else opened the results: there were no shortcut buttons for them,
  and no find state survives a restart (library.json keeps the page and
  zoom, not the panel).

Model tests cover the rail's items, which item is lit while results show,
and the left side a search gives back. ui.py's shell journey checks the
rail. Its light pass shows Done giving back the Comments panel. Its dark
pass shows the results' cross folding the left side again. The README notes
in English and Chinese that ⌘F's stand-in is the field itself.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The user: "there is no full screen read mode?". Design 02's floating bar
ended with a full-screen button; we had built fit width and fit page
instead.

The bar now ends with it, and it opens Read mode: the pages alone.
- Hidden: the document tabs, the mode bar, the rail and both panels, the
  tool row and the status line. Each hides through a plain wrapper, since a
  view whose on_render starts hidden stays hidden.
- The pages get the whole window (canvas_size). The bar keeps previous and
  next, "3 / 24", the zoom, fit width, fit page, and "Exit read mode".
- Scripts get no key events, so there is no Esc. A quiet, always-clickable
  control at the top right leaves too.
- A tap on a page only chooses it: no tool's marks or editors show, and
  nothing is marked, filled or edited.
- The page and the zoom the person chose carry across entering and leaving
  (kept_zoom). A fitted or automatic zoom fits the new canvas, and so does a
  resize while reading.
- Both styles draw it from the theme's tokens.

No app can make the OctoSense window itself full screen; there is no host
API for it. Both READMEs say so under Known: maximize the window for the
full effect. A "Read mode" entry in the Read tab's controls does not fit
naturally (the Read tab has no control row), so the bar's button is the way
in.

Model tests cover which regions show, the canvas in and out of Read mode,
and the zoom kept. ui.py's shell journey enters it from the bar, turns a
page and zooms in, then leaves: by Exit read mode in the light pass, by the
corner control in the dark pass. It checks the page and zoom carried over.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #479 (M2): pdf.comments read the whole document again on every
call, on the UI thread (#399). Typical documents took 1 to 8 ms. It
measured about 90 ms at 20k pages, about 230 ms with 100k comments, and
about 100 ms for a 115 MiB file with a broken xref, which is rebuilt on
every call.

The dates read now stay on the open document, with a weak reference to the
working bytes they came from. The engine swaps those bytes for new ones on
every edit, undo, redo and save. The weak reference keeps their address from
being reused, without keeping the bytes, so a kept read matches only the
version it came from: right by construction. A second pdf.comments with no
change reads nothing; the next one after a change reads once.

Tests:
- a second call reads nothing, and an edit, an undo, a redo and a save
  each read once (a test-only counter);
- hostile dates (é where a date's digits or its zone go, and alone) are
  read without a panic: a zone that is not one is no zone, and text that is
  no date stays as written (review L4).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #479 (M1, L1–L4). The library learned a PDF's pages once and
never again. Before #479 every start reopened each PDF; with kept covers,
nothing refreshed them. So a 4-page PDF with a page deleted and saved said
"4 pages" on Home after a restart, and Combine said "All (4 pages)". A
range of "4" then passed the app's check, and the merge was refused.

- A save records the open document's page count in the library, and marks
  the cover stale.
- Once a start, Home asks pdf.cover for each PDF it shows. A kept cover
  answers from its file alone (a stat and a 24-byte read). A cover at a new
  path means the PDF changed, so its pages and size are read again from
  pdf.info (L2).
- pdf.cover's too_large (a first page over 16 megapixels even at 48 dpi) no
  longer marks the PDF "Couldn't open this PDF" and drops it from Combine's
  and Insert's pickers. Like storage_full: no cover, pages and size from
  pdf.info (L1).
- Home asks covers only for the first 32 PDFs it shows, as many as the
  service keeps. Asking for more evicted the first ones every start, so every
  start re-rendered everything. PDFs beyond those get their pages and size
  only (L3).

ui.py's restart run now removes a PDF from Home and checks that its kept
cover goes with it (L4). Before, the full run, which removes a PDF, had no
covers to check. Both READMEs describe the covers as they are now.

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

Review of #479 (L4):
- apps/AGENTS.md described the pdf service's render cache as .cache/pages/
  alone. It now names pdf.cover, .cache/covers/, the 32-cover cap within the
  16 MiB / 64-file cache, page renders going first, and comment dates read
  once per version of the document.
- The Chinese files README's files.status row now lists the three older
  fields its English row has: photo_pick_supported, text_share_supported
  and max_share_text_bytes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A dark pass at --budget-ms 1000 lost the tap on Add files: the list of PDFs
to add never opened, and the next step found no "Board minutes.pdf". The
same step passed in that run's light pass and in every earlier dark pass.
The driver now taps Add files until a PDF of the list shows, at most three
times. It taps again only while none shows, so an open list is never tapped
closed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ew's fixes

ui.py's shell, restart, full and empty runs, in a hidden desktop shell with
the real pdf engine, at the window's own size (an app area of 1292 x 662),
light and dark, just after midnight on 11 Oct; and the fixture run in
card-host at the designs' 1536 x 1024. The frames replace the first round's:
- Read mode, entered on page 2 and left on page 3 at 125%, by the bar's Exit
  read mode (light) and by the corner control (dark).
- Find from the top bar alone: the rail is Pages, Outline and Comments. Done
  gives the Comments panel back; the results' own cross folds the left side
  again.
- Remove deletes a PDF's kept cover after a restart.
- Comment dates read "Today 00:34" at 07:34 UTC.
- Storage, covers, Combine's footer and Edit's Alignment and Colour, as before.

The four runs used --budget-ms 1000. At the runtime's 64 ms, two attempts
passed every functional step of the shell journey, light and dark, and then
failed the log check on script budget overruns. Each overrun came with a
UI-thread stall in a pdf service file rename: 555 ms in pdf.save, then
262 ms in the render cache. App Hub runs host services inline on the UI
thread (#399), and this branch doesn't change those paths. The load average
was 54 to 74 then. run-64ms/ keeps the third attempt's logs and the lines
read from the second's before the third overwrote it. The README and
receipt record this, and the first 1000 ms run, which lost a tap on
Combine's Add files.

The runs were made on 12e0e4cb, on main 07b63da. The branch was then
rebased onto main bcd52cf, which brought in #484 alone (App Hub's pin
moved from d86101d7 to c317f787). The runs were not repeated on it;
setup.py --check --cargo, native_apps.py --check and the pdf service's tests
pass on it.

The app README's Verified and Known entries say the same, in English and
Chinese.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ymote
ymote force-pushed the feat/pdftools-v2-followups branch from 4806eb5 to 177bdf1 Compare October 11, 2026 13:53
@ymote
ymote marked this pull request as ready for review October 11, 2026 14:07
@ymote

ymote commented Oct 11, 2026

Copy link
Copy Markdown
Contributor Author

Local CI passed on 177bdf1 (tools/ci-local.sh --only all --linux-host --offload, 14m23s, Darwin arm64; the ubuntu jobs and the macOS jobs' portable steps (their macOS steps ran here) on the Linux build host, Linux x86_64).

GitHub would run for this PR: desktop.yml, phone.yml, apps.yml. Merged on this local pass (macOS runner queue); GitHub CI runs on the merge commit on main.

Job Step Result Time
ci-local / drift The local mapping fits the workflows PASS -
desktop.yml / desktop Prepare the pinned framework sources PASS 0s
desktop.yml / desktop Compile the desktop with the shared runtime PASS 3m28s
desktop.yml / desktop Desktop tools and scripts PASS 16s
desktop.yml / desktop (linux) Prepare the pinned framework sources PASS 0s
desktop.yml / desktop (linux) One copy of every shell source PASS -
desktop.yml / desktop (linux) In-app updater and installer handoff PASS 1m47s
desktop.yml / desktop (linux) One Makepad, App Hub, octos and Rinx in the locked graph PASS 2s
desktop.yml / desktop (linux) The native apps' generated places match native-apps.json PASS 0s
desktop.yml / native-host-api Prepare the pinned framework sources PASS 1s
desktop.yml / native-host-api Build the native acceptance host and pinned Hub CLI PASS 49s
desktop.yml / native-host-api Signed app tool calls a real OS permission API PASS 6s
phone.yml / home Prepare pinned sources and the reviewed runtime patch PASS 0s
phone.yml / home Validate launcher icons and Apple asset catalogs PASS 3s
phone.yml / home Compile Home and its bundled modules PASS 5m13s
phone.yml / home (linux) Prepare pinned sources and the reviewed runtime patch PASS 1s
phone.yml / home (linux) Verify the single runtime graph PASS 3s
phone.yml / home (linux) The native apps' generated places match native-apps.json PASS 0s
phone.yml / home (linux) The pinned octos revision PASS -
phone.yml / home (linux) Build octos (unless cached) PASS -
phone.yml / home (linux) Build Home, App Hub admission and runtime policy tests PASS 3m25s
phone.yml / home (linux) Test Home, App Hub admission and runtime policy PASS 41s
phone.yml / home (linux) Two-lane scenario (real kernel, scripted model) PASS 28s
phone.yml / home (linux) Build opt-in AppCard tests PASS 2m43s
phone.yml / home (linux) Test opt-in AppCard PASS 1s
phone.yml / home (linux) Build craft-engine relay and system-chat tests PASS 2m48s
phone.yml / home (linux) Test craft-engine relay and system chat PASS 4s
phone.yml / home (linux) Build linked Terminal tests PASS 2m26s
phone.yml / home (linux) Test linked Terminal PASS 1s
phone.yml / home (linux) Build Wasm and runtime policy tests PASS 1m16s
phone.yml / home (linux) Test Wasm and runtime policy PASS 11s
apps.yml / services (linux) python3 tools/setup.py PASS 0s
apps.yml / services (linux) Shared system app interface PASS 0s
apps.yml / services (linux) Kernel service PASS 20s
apps.yml / services (linux) OctoSense release updater PASS 7s
apps.yml / services (linux) App peers PASS 30s
apps.yml / services (linux) AI providers config PASS 6s
apps.yml / services (linux) Connected provider protocols and Linux host adapter PASS 48s
apps.yml / services (linux) L0 in-card chat (sys.chat, host side) PASS 13s
apps.yml / services (linux) System toolbox (workflow templates) PASS 1m39s
apps.yml / kernel-security (linux) python3 tools/setup.py PASS 0s
apps.yml / kernel-security (linux) The pinned octos revision PASS -
apps.yml / kernel-security (linux) Build octos (unless cached) PASS -
apps.yml / kernel-security (linux) Real-kernel tests PASS 40s
apps.yml / apps (linux) python3 tools/setup.py PASS 0s
apps.yml / apps (linux) Host services (Mail, AI providers, News) PASS 2m18s
apps.yml / apps (linux) Connected accounts and Rinx editor PASS 1m40s
apps.yml / apps (linux) Shell AI services (crates/ai-host) PASS 2m51s
apps.yml / apps (linux) AppCard runtime setup safeguards PASS 0s
apps.yml / apps (linux) AppCard clippy (own crates; shared runtime compiles) PASS 28s
apps.yml / apps (linux) AppCard transport and store PASS 8s
apps.yml / apps (linux) One Makepad, App Hub, octos and Rinx in the locked graph PASS 3s
rom.yml / product (linux) python3 -m unittest discover -s tests -v SKIPPED (!) -
rom.yml / product (linux) Check generated Agent Binder client SKIPPED (!) -
rom.yml / product (linux) bash -n scripts/stage-forks.sh scripts/publish-release.sh scripts/apply-to-tree.sh scripts/build-rom.sh PASS -
rom.yml / web-installer (linux) npm ci --ignore-scripts --no-audit --no-fund SKIPPED (!) -
rom.yml / web-installer (linux) npm test SKIPPED (!) -
rom.yml / web-installer (linux) npx playwright install --with-deps chromium SKIPPED (!) -
rom.yml / web-installer (linux) npm run test:browser SKIPPED (!) -
rom.yml / web-installer (linux) python3 -m unittest discover -s tests -p test_installer_manifest.py -v SKIPPED (!) -
rom.yml / web-installer (linux) bash -n scripts/release.sh SKIPPED (!) -
linux-host / sandbox (linux) python3 tools/setup.py PASS 0s
linux-host / sandbox (linux) Process sandbox on Linux (Landlock, seccomp) PASS 1m52s

Skipped (these did NOT pass; (!) marks a skip that blocks tools/ci-local-merge.sh):

  • rom.yml / product (linux): python3 -m unittest discover -s tests -v: javac -version does not work here: install a JDK (17 or newer) and put its bin/ on PATH or set JAVA_HOME
  • rom.yml / product (linux): Check generated Agent Binder client: needs build-tools/35.0.0/aidl, platforms/android-35/framework.aidl in the Android SDK (no Android SDK found): install the SDK packages 'build-tools;35.0.0' and 'platforms;android-35' (sdkmanager) and set ANDROID_HOME
  • rom.yml / web-installer (linux): npm ci --ignore-scripts --no-audit --no-fund: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH
  • rom.yml / web-installer (linux): npm test: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH
  • rom.yml / web-installer (linux): npx playwright install --with-deps chromium: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH
  • rom.yml / web-installer (linux): npm run test:browser: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH
  • rom.yml / web-installer (linux): python3 -m unittest discover -s tests -p test_installer_manifest.py -v: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH
  • rom.yml / web-installer (linux): bash -n scripts/release.sh: no node, npm, npx on PATH (the workflow uses node 24); put node on PATH

Notes:

  • GitHub runs Python 3.12; this run used Python 3.9.6
  • GitHub runs Python 3.12; the Linux host used Python 3.14.4

Native GitHub acceptance passed on the same head: embedded-browser.yml.

@ymote
ymote merged commit 5cab54a into main Oct 11, 2026
3 of 9 checks passed
@ymote
ymote deleted the feat/pdftools-v2-followups branch October 11, 2026 14:08
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.

1 participant