Skip to content

Pi Pocket 0.20.0: theme fonts, public artifact links, inline cards, and fast long sessions - #125

Merged
TannerMidd merged 72 commits into
mainfrom
release/0.20.0
Oct 22, 2026
Merged

TannerMidd merged 72 commits into
mainfrom
release/0.20.0

Conversation

@TannerMidd

@TannerMidd TannerMidd commented Oct 21, 2026 •

Copy link
Copy Markdown
Owner

Security

  • Update undici from 8.10.2 to 8.12.3. Version 8.10.2 has 4 high-severity advisories of October 15, 2026, among them a TLS certificate validation bypass. An npm override puts every copy on 8.12.3, also the copy that Pi's packages pin.
  • Update DOMPurify from 3.4.16 to 3.4.17, for a critical advisory.
  • The launcher warns when Node.js does not have the security fixes of October 21, 2026. Update to Node.js 22.23.4, 24.22.1, 26.12.1, or newer.
  • The Docker service sets no-new-privileges.
  • A presence time that cannot be saved (for example, on a full disk) no longer stops the server.

For extension authors

All new members are experimental. They can change in a minor release. Make sure that a member is there before you use it.

Update

You must restart the server after you update (Menu → Restart server). Run npm install first: the dependencies changed. To get a new Node.js, close the launcher and start it again: Restart server keeps the Node.js that runs now.

Full changes: v0.19.0...v0.20.0

Review

Final review of the combined branch: 3 code reviews (security, server, web and docs), 3 live tests (UI on a copy, agent features on the live server, install on 6 Node versions), and a fresh review of the fixes. Every blocker, high, and medium finding is fixed with a test that fails on the old code. npm test: 518 of 518 pass. Docker was not built: its daemon does not run here.

vitalNohj and others added 30 commits October 21, 2026 03:38
Themes already choose the fonts and ligatures (--sans, --mono,
--ligatures). This gives them more to choose from, and fixes two gaps when
a theme changes the font.

- The app bundles Inter, Fira Code, IBM Plex Mono, and Iosevka, which a
  theme names in --sans or --mono ("Pocket Inter", ...) instead of shipping
  font files of its own. A browser downloads only the ones a theme uses.
- Real italics: JetBrains Mono, Inter, IBM Plex Mono, and Iosevka bring
  their own italic files, so code comments and emphasis are no longer
  slanted by the browser. The default look gains JetBrains Mono's italic.
- The Nerd Fonts icons that prompts and tools print, as a font of icons
  alone in the default --mono, fetched only when an icon shows.
- No flash on reload: the page preloads the app's fonts a theme names, as
  it does the theme's own. Before, it preloaded Geist and JetBrains Mono
  and then switched.
- The message box and the status line measure again when a theme's
  stylesheets or a font finish loading. Before, a theme with a wider font
  left the box too short for its text until the next keystroke.

web/fonts/SOURCES.md says where each font came from and how it was
checked; Iosevka and the Nerd Fonts symbols are trimmed, with every name
kept. The theme guide lists the fonts a theme can name.
Under the theme tools, the owner picks the theme in use's Text font,
Code font, and Ligatures, with a preview. Each choice is saved into the
theme (its fonts.css, which theme.css imports first, and whose values
win over the theme's own), so everyone who uses it gets it at once. A
theme that comes with the app is duplicated first; a theme of colors
alone gets the stylesheet and the theme.json it needs.
- "Fonts for <theme>" is always there: a theme of the owner's has the
  menus; one that comes with the app shows its fonts, with Duplicate to
  change.
- System Sans and System Mono, the device's own fonts, in the menus.
- Text size: 13, 15, 17, or 20, or any size from 10 to 32 px typed in,
  in half pixels.
Share in the artifact viewer publishes a version, or the artifact's
newest, to `/pub/<token>`, which needs no sign-in. The page is the same
sandboxed one `/a/…` serves, so it still cannot reach the app or its
cookies.

A share stays at the version it was made from, or follows the newest,
and lasts until it is ended or for a day, a week, or a month. Ending it
makes the address answer nothing. Only people who see every session
publish or end one, never someone invited to a single session.
Show when Pi last replied so users can judge how long a
conversation has been idle. Cache retention varies by provider,
so we use message time without estimating cache expiry.

Signed-off-by: Marc Lopez Rubio <marc5.12@outlook.com>
Opening a session that ends in a long run of Pi's alone froze on a phone. Every group after the last person's message is open, and an open group drew all of its steps: in a real session, 4 groups held 106, 132, 33, and 97 steps, about 2,300 of the page's 2,734 elements and 17 screens of height.

An open group now draws its newest 24 steps, with "Show N earlier steps" above them for the rest. A group shown in full by hand, or by a jump to one of its answers (Find, a pin, Changes), stays so for as long as the page is open. The hidden steps still name the ones after them, so their keys do not change when the rest shows. The button is a theme part, `timeline-earlier`.

That session now has 1,106 elements and 5,190 px of height. At a phone's speed (4x CPU slowdown), opening it is drawn in 312 ms instead of 500 ms, and its longest task is 213 ms instead of 353 ms.

A new test in test/mobile-ui.test.ts checks the 24 steps, the button's count, and that it shows all 30.
Opening a session laid the page out twice. The message box measured its text as it came (`fitBox`), which made the browser lay out the whole page in the middle of the render; `useSide` then did the same after every draw, for the sidebar's resize handle and the drawer. The frame laid it out again. On a phone, this was 100 to 230 ms of each switch.

The message box now sizes itself in browsers with `field-sizing: content` (Chromium 123 and later), with the same least height, growth, and 40% limit; elsewhere `fitBox` measures it a frame later, before the frame is painted. `useSide` measures with a ResizeObserver, when the browser lays the element out anyway (as it comes, shows, or changes size), and still on a window resize or a theme change.

The heights of the message box were checked by hand against the old code, empty, with 5 lines, past the limit, cleared, and wrapped on a phone.
A full view sent every entry of the active context in one event: 8 to 10 MB
for a session of 6,000 entries. On a phone the tab stopped for most of a
second to read it each time the session opened, and it draws only the newest
rows.

A tab of protocol 4 now gets the order of every entry, but only the entries
of the newest 80 rows (the transcript draws 60, PiP 80), with the turn the
oldest is in. Earlier ones come from GET /api/c/<id>/context when the person
shows earlier messages, opens Find, or jumps to a message not loaded yet. The
server counts what it left out as sent, so updates never send it, and a full
view of the same session keeps what the tab loaded.
Run Pi Pocket in a container from a thin Node image with Chromium, git, gh,
ripgrep, fd, and cloudflared. The database, extensions, themes, Pi's folder,
and the workspace are bind-mounted from the host, so they survive rebuilds.

- docker/Dockerfile, entrypoint that clears stale Chromium profile locks
- docker-compose.yml with LAN defaults and healthcheck
- .env.example for ports, access mode, folders, and optional provider keys
- docs/docker.md, linked from the README and docs index
- .gitignore, .prettierignore, and eslint ignore the compose ./data folder
Extensions could shape requests and read models, but not add a provider
of their own: a module that brings a new service had no way to give its
models to Pi. `host.registerModelProvider(provider, config)` registers
Pi's own `ProviderConfig` with the model runtime, and its return value
removes the provider again.

The cleanup leaves a newer registration alone: a reloaded module starts
before the old version's cleanups run, so without the guard an edit
would take out the provider the new version just put in. The provider's
models appear at Pi's next model refresh, as with Refresh models.

Docs and types say it is experimental in 0.20.0. The test loads a
drop-in that registers a provider, checks a reload keeps it, and checks
that turning the module off removes it (it fails without the guard).
Adds a Dockerfile, a Compose file, and docs/docker.md. The database,
extensions, themes, Pi's folder, and the workspace are bind-mounted
from the host, so they survive rebuilds.
Thanks to Luigi Maselli (@grigio) for the change.
Review fixes for #119.
- Dockerfile: drop CMD --access lan, which overrode PI_POCKET_ACCESS
  from .env. CMD [] keeps the node image's CMD out of the launcher.
- docs: local mode listens on the container's 127.0.0.1, so the host
  cannot reach it. For this machine only, bind 127.0.0.1 and keep lan.
- docs: in lan mode the printed link names the container's address;
  use the host's. Tailscale and Cloudflare notes made accurate.
- docs-site: list docs/docker.md in the sidebar; the site build fails
  on a page that is not listed.

Tested: npm run check, lint, format:check, docs-site build. Not tested:
an image build (no Docker access on this machine).
…actions

Adds four experimental pocket members, and WebRequest.sessions on the server:
- pocket.sessionChips: chips under a row's title; a chip with run is a button.
- pocket.sessionSearch: words that the list's search and the launcher match.
- pocket.sessionFilter: a button in the search field; while match is set, the
  list shows only what every filter that is on keeps.
- pocket.sessionAction: an action in a held row's sheet and the selection bar.
- request.sessions(): the sessions the person can see, so routes check ids with
  core's own rule.

A function that throws stops its module, and the list draws without it.
Theme parts: session-chips, session-chip, session-filter.

From the review of #118 by Jeroen Dee (@jeroendee): the mechanism goes to core
as extension points (C7), and tags become an extension.
Shared tags on sessions, built only on host, web, and the session list's
extension points: chips on rows, a tag filter beside the search (a tap on a
chip filters too), tags in the search and the launcher, and Edit tags for a
held row, selected rows, or the open session. Tags are lowercase, 32
characters, 10 a session at most; a change is all or nothing. Routes check ids
with request.sessions(), and each person gets only the tags of sessions they
see. Viewers read and filter; people invited to one session do not load it.

Based on #118 by Jeroen Dee (@jeroendee), which first put tags in core.
Thanks to Jeroen Dee for the feature, its design, and its limits.

Tested: test/session-tags.test.ts, test/session-list-extensions-ui.test.ts,
npm run check, lint, format, docs-site check, and the full npm test.
- A filter kept from before is off until the tags load, and off when they fail:
  it no longer hides every session. Only the newest load of tags counts.
- Sheets from a filter or a session action open over the drawer, which stays.
- sessionAction takes access ("steer" by default, or "view"); labels are cut
  to 24 characters. The first 3 filters show and count.
- SessionInfo has updatedAt, as WebSession does.
- The filter button says its state in data-state and its label, not
  aria-pressed (a tap opens a sheet). Chips leave the tab order, and while
  taps select rows a chip tap selects too.
- A linked row's branch meets its title line; chips' touch area stays in the
  row. The empty list says when the filters hide everything.
- Tag names use the server's rule in the browser too; a clear error past 500
  sessions; hover only where there is hover; theme radii.

Tested: npm run check, lint, format, and the full npm test (394 pass).
Splits #118 by Jeroen Dee (@jeroendee): the session list gets experimental
extension points, and tags become a built-in extension, off by default.
Thanks to Jeroen Dee for the feature and its design.
Set the package version. Session tags reads it to know that the server has what it needs.
- sessionAction takes icon or iconPath: the first such action is also a button
  on rows, beside archive, pin, and mark unread. Session tags' Edit tags is one.
- sessionFilter takes started(id): New in the Sessions head, while the filter
  is on, gives the new session to it. Session tags tags it with the chosen
  tags, so it stays in the filtered list.
- Session tags shows what a save answered at once, and looks again when the
  tab comes back: an event sent while the tab reconnects is lost.
- On a server older than 0.20.0 the module adds nothing. Such a server loads
  the file when its checkout moves, and would turn it on without the core it
  needs. A server that runs 0.20.0 turns it on and off with no restart.

Both are from #118 by Jeroen Dee (@jeroendee).
Tested: npm run check, lint, the new and changed tests, and the full npm test
(395 of 397; the 2 timing tests that failed under load pass alone).
Two findings from a test with a real dynamic provider (Charm
Hyper): a provider without a static models list shows nothing
until a network refresh, and a refreshModels that ignores
allowNetwork and returns [] empties the catalog on every local
pass, before the network pass can restore it. The reference now
tells authors about both: ship a fallback models list, and
return the stored catalog when allowNetwork is false.
…es now as an Interrupt

A queued message tapped open shows "Steer now" (on a Queue item) or "Move to
queue" (on a Steer item), and "Interrupt". Interrupt asks for a second tap
("Interrupt?") for 4 s; CONFIRM_INTERRUPT in web/composer.js turns that off.

Only the message's sender sees the buttons, and the server refuses anyone
else. The composer's Steer / Queue / Interrupt picker does not change.

The server moves the inbox item's mode in one commit. For Interrupt, the same
commit takes the message out of the queue and starts a background task that
stops the run and sends it, so a restart cannot lose it. Like the composer's
Interrupt, stopping the run withdraws the rest of the queue.
A jump counted an entry as loaded when the tab held it, but after a gap
(a reconnect after more than 80 new rows, or a compaction) the transcript
does not draw held entries above the gap, so the jump said the message
was in earlier history. isLoaded() now checks the entry is in the run the
transcript draws, and loadEarlier and jumpToEntry use it.

Tested: node --check, prettier, eslint, test/app.test.ts and
test/mobile-ui.test.ts (42 pass).
- Interrupt marks subagents' reports withdrawn in the same commit that
  takes the message out of the queue, so a restart between the two can't
  let the reports go out again.
- If the interrupt task can't stop the run or send the message, it now
  says so with a warning instead of faulting without a word. The message
  has already left the queue by then.
- A missing or bad submissionId gets a 400, not "Only its sender".
- The confirm tap clears the Interrupt? state, so a quick third tap
  doesn't send again and toast a 409.

Tested: npm run check, lint, prettier, npm test (401 pass).
A tab of protocol 5 gets the first view's entries without what shows only
when a step is opened (a thought's words, a tool's long input and output,
large details), marked partial, and the whole entries in the update right
after. The first part of a long session is tens of kilobytes rather than
hundreds, so its rows show sooner on a slow network. A long poll answers
the first time as soon as the first view is built.
If a tab left between the light first view and the whole update, and Pi
added rows meanwhile, the light entry ended up past the newest rows on
reconnect. The server counted it as sent, mergeView kept the held light
copy, and loadEarlier let it win over the whole one it fetched. The
thought could not open and the step had no outcome until a reload.

A full view of the same session now drops held light entries, so the gap
gets loaded, and loadEarlier lets a fetched entry replace a light one.

Tested: npm test (403 pass), lint, prettier, and a reload on a test copy
leaves no partial entries.
Pocket now keeps a list of registrations for each provider name, newest
last. Removing the newest one puts back the one before it, so these work:
a reload that fails after it registers, two modules that use one name, and
a config Pi refuses (it throws, and leaks nothing). The previous registration
is unregistered first, so Pi does not merge the old config into the new one.
After each change Pocket refreshes that provider locally and sends the new
model list to open tabs.

The docs now say that the last registration wins. They tell authors to pass
the removal to web.onDispose, and warn that a built-in provider name with
another baseUrl sends the owner's stored key there. docs/extensions/host.md
has a line about registerModelProvider.

Tested: 5 new tests in test/model-providers.test.ts (reload merge, failed
reload, refused config, two modules, tab model list). All fail without the
app.ts change and pass with it. model-refresh, providers, and
extension-loader tests pass. npm run check and npm run lint pass.
It publishes HTML into the artifact store like the artifact tool, but its result is marked inline and names the version it made, so a card can keep showing that version. HTML only: a Markdown or SVG artifact stays with the artifact tool. Plan mode, PiP, and the built-in tool list know the new name; Pi's guide says when to use each tool.

Tested: test/artifact-inline.test.ts (create, update, old result keeps its version, replay, edits and limits, fork, who may read the page, restart).
The call leaves its group of steps and sits between Pi's words as a card: the html block with Preview/Code and Run/Stop, the artifact's name and version in its bar, and an Open button for the viewer at that version. The card reads its own version's page from the artifact's address, so an update adds a card and leaves earlier ones alone. A running card gets the artifact bridge (follow-up, saved state under the artifact's id). A failed or denied call shows its step. On a phone the bar takes two lines.

Tested: test/artifact-inline-ui.test.ts in Chromium (placement, Preview/Code, Run/Stop, sandbox, bridge, update, Open, reload, phone, failure, a viewer, a session-only guest).
Tested: npm run check and lint.
The inline tool now ignores a path or type the model sends, and a replay of an inline call finds its version even after the artifact became Markdown. An answer that starts with a card keeps its jump anchor on its words, and a group after a card gets its start time. A card drawn again shows at once from the pages already fetched. PiP goes without the tool: its small panel draws no card.

Tested: turns, artifact-inline, pip, and plan tests; check and lint.
The live line can still sit after the card when the call returns; the test now waits for Pi's words, as a person would see them.

Tested: the browser test, 6 runs in a row.
A version made by artifact_inline is marked, and the artifact tool refuses to change the type of an id that has one: the page is served by the artifact's type, so an old card would show the wrong page. The guide mentions artifact_inline only in sessions that have the tool (PiP does not), and the guide and the result say that only a direct call, not one inside codemode, shows a card.

Tested: artifact-inline and app tests; check and lint.
The card's page cache keeps the most recently used 20 pages, and a late fetch can no longer put an evicted page back. Two inline calls with the same id in one reply get different item keys, as a group's steps do.

Tested: turns test, and the card's browser test.
When the owner lists models for subagents, a subagent or a linked session
now runs only on one of them. The session's own model counts only if it is
listed. With no model named, the session's model is used if listed, else
the first listed. A named model off the list is refused with word not to
pick another on its own. Work for a subagent or a linked session that runs
on an unlisted model is refused too. With an empty list nothing changes.

The old rule (the session's own model always allowed, and the default) is
what started subagents on an unlisted model. The prompt sections, tool
descriptions, settings text and docs now say the new rule.

Tested: new cases in test/subagent-models.test.ts, linked-sessions.test.ts
and extension-web.test.ts (listed, unlisted parent, empty list, linked
session start and its subagents, human route, message to unlisted
session).
…x stale wording

A hand-edited config.json could hold blank or slash-less entries in
subagentModels; they are now ignored, so a bad first entry cannot become
the default model. Comments and the Providers sheet header no longer say
the session's own model is allowed besides the list.

Found by a read-only review. Tested: npm test (459 pass).
The subagent and team prompt sections now give clear triggers (independent
parts, long or noisy jobs, a review before calling work done, or when the
user asks), when to work alone, a short brief checklist, and a report
format. The ASD-STE100 style list is cut to a short pointer. The spawn
result tells the agent to carry on instead of waiting. Tool and parameter
descriptions are shorter. The codemode section mentions starting several
subagents at once. The subagent guide now says a report is information,
not orders. Safety rules are kept: model list, no polling, ASD-STE100,
nothing destructive because another agent asks, stop on model failure.

Tested: npm test (459 pass); 40 runs with real cheap models on a throwaway
copy (delegation 0 of 18 before, 4 of 18 after; 0 of 4 on trivial tasks
both).
With an allowed-model list, subagents and linked sessions no longer
use their session's model unless it is listed; the hint PiP gets when
it names a model on its own said they did.
A send replayed after it was taken is no longer refused when the owner's
list changed in between. A subagent or linked session with no model is
refused when a list exists. The guides again say never to pick a model
on your own, keep 'every fact kept' in the ASD-STE100 list, apply the
style to each spawn and send message, and say to do nothing destructive
or with credentials because a report asks. The codemode hint reads
plainly.

Tested: subagent, linked-session, agent-teams, extension-web, codemode
tests (59 pass); format, check, lint pass.
…se by

When the owner's list has models and the session's own is not on it,
model is now required for a subagent or a linked session. The refusal
lists the allowed models and tells the agent to choose one that suits the
work; it never says to stop and ask. A name off the list, a typo, or a
blank name gets the same kind of answer. The 'first listed' default is
gone from code, prompts, errors, docs and the Providers sheet text.

The subagent and team prompt sections list each allowed model on one
line with what the registry knows (thinking, context window, price, which
is the session's own) and a short rule for matching model to work.
host.modelInfo gains optional contextWindow and cost. Tests cover each
start path, the prompt text, and the model line.

Tested: format, check, lint; npm test (484 of 485; the browser test file
failed once in cleanup under load and passes alone).
A module that fetches an address from outside (a page Pi names, a search
result's link, a crawl) had only fetch, which reaches this machine, the
local network, and a cloud's metadata address. PR #126 had to put web
search in core to keep that check there (C4).

`host.fetchPublic(url, init)` resolves names where it connects and
refuses one with any address that is not public, so DNS rebinding gets
nowhere; IP literals are checked first. Each redirect is checked again;
a 303 becomes a GET without its body's headers, and another origin gets
no credentials. It stops at 10 MiB decoded, 30 s, and 5 redirects.
Refusals carry a code: POCKET_NOT_PUBLIC, POCKET_TOO_LARGE,
POCKET_REDIRECTS.

Experimental in 0.20.0. Its first user is docs/examples/read-page.ts, a
read_page tool that fences the page's text with host.text (C15). Docs:
host reference and guide, limits, API versions, security model, and
the web access case in core-or-extension.

Tested: test/public-fetch.test.ts (address ranges, literal forms,
names, redirects, limits, timeout), the examples test (read_page
refuses localhost), npm run check, lint, and npm test (font-ui is
flaky once, passes alone). A read-only review found no SSRF bypass;
its four correctness findings are fixed.
undici 8.10.2 has five high advisories (Oct 15, 2026), among them a TLS
validation bypass in Agent. An override keeps Pi's pinned copy on 8.12.3 too.
DOMPurify 3.4.16 has a critical advisory. The launcher now warns on a Node
release without the Oct 21, 2026 security fixes (22.23.4, 24.22.1, 26.12.1).

Tested: npm run check, lint, test (492 pass) on Node 26.12.1; npm audit clean.
…eal tap

The frame's first script opens a channel to the app and keeps it, so a page
the frame goes on to cannot use the bridge, and a pending answer cannot reach
it (SEC2, WEB-001). A follow-up needs a trusted tap or key on the page within
5 s, one each; focus no longer counts (SEC1). A selection cannot end the JSON
fence (WT-02). A non-string schema gets one error for get and set (LUI-5).
artifact_inline's words: no path in its error, and no card inside codemode
(LAG-2, LAG-4). A preview with an inline SVG fits (LAG-5).

Tested: the artifact bridge, inline, and state tests, including focus with no
tap, a navigation with a slow image, and a held answer across a navigation.
Moving one queued message to Interrupt took every other queued message with
it, from everyone (LUI-1). Now the others are queued again after it, in order,
with their senders and modes, once each across a restart. A resumed interrupt
stops only the run it was made for (SRV-5). People move their own queued
message as they withdraw it, also right after a restart (SRV-4).

Also: a last-seen time that cannot be saved (a full disk) is logged, and no
longer stops the server (LUI-11).

Tested: test/app.test.ts, with 3 queued messages from 2 people, a reopen with
take turns on, a replayed interrupt, and a failing save.
A public address of one of this machine's interfaces passed the range check
(F1). It is refused now, for literal URLs, every DNS answer, and redirects.
The docs say what is still not detected: NAT reflection and internal public
ranges.

Tested: test/public-fetch.test.ts with stubbed interface lists.
- A key that a module's composerKey takes no longer runs the app's shortcut
  too (WEB-002, W1, LUI-2).
- Actionable session chips take keyboard focus (WEB-003). An async chips,
  words, or match is a module error, and a failed filter hides nothing (W8).
  Extension search words are cached per session (WT-07).
- Fluid: a skipped transition throws nothing (LUI-4), and the sidebar's own
  transition is off during the morph (WT-04).
- Jumps follow the app's Motion setting (LUI-10). The launcher finds Peeks
  (LUI-3). Text size has a group name, and ligatures can go back to the
  theme's own (WT-09, WT-11).

Tested: the composer-keys, session-list, launcher, and font UI tests.
- With a model list, a linked session could not send its report back to the
  session that started it, when that one ran on an unlisted model (SRV-1).
  A reply to the source is not work: it goes on any model now.
- The undici override is a plain version: npm 10 (Node 22) could not resolve
  $undici without a lockfile (INS-2).
- Docker: the service sets no-new-privileges (MISC1, part).
- Tests: cleanup retries, and only warns, while a browser's helpers still
  write (SRV-2); themes.test.ts removes its folder; the browser frame test
  waits for the first frame (SRV-3).

Tested: linked-sessions, themes, browser tests; docker compose config.
limits.md lists the new limits; map.md the container files and every module;
architecture.md what a light entry hides; the session-list guide its fields,
and that readers answer at once; composerKey's aliases; the Fluid sentence as
it works; the README's new features; the queue and the artifact bridge as
they work now (WD-1..6, WEB-004..008, W5..W9).
…ps only its run

- withBridge skipped leading comments with a pattern, and a browser ends an
  odd one (<!-->) elsewhere: a script hidden there ran before the bridge,
  wrapped its tap listener, and sent a follow-up with no tap. Now only a
  doctype at the very start goes before the bridge; nothing is skipped.
- InterruptTask checked the run, then stopped whichever run was going: one
  that started in between was stopped. It now stops the run it was made for
  by its task id (harness.abortTask), and nothing when that one ended. So
  messages queued after the move stay queued too.

Tested: withBridge cases with odd comments, a real-browser test of the
<!--> attack, and a test that ends the run and starts another between the
task's look and its stop; both fail on the old code.
- The bridge counted a tap at both pointerdown and mousedown (and pointerup
  and touchend): a page spent the tap in one and got it back from the next,
  for a second follow-up 2 s later (WEB-009). Now one event counts for each
  tap: a mouse's press, a finger's or pen's lift, a key's press without its
  repeats.
- The model and branch pickers, the file tree and its search, and the
  subagents board took Alt+↑/↓ with plain arrows; with the new
  defaultPrevented check, that stopped the session switch there (WEB-010).
  They let Alt arrows through now; the folder picker keeps its Alt+↑.
- architecture.md: why a short result shows its outcome (WEB-011).
- The odd-comment bridge test ran after the test that closes the page; it
  runs before it now.

Tested: the bridge script against one full click, touch tap, and held key
(fails on the old code); Alt+↓ in the model menu switches sessions (fails on
the old code); artifact-bridge-ui 10/10 twice; composer-keys-ui.
… gets the bridge

- With take turns on, someone who was not driving could move their own
  queued message to Interrupt and stop the driver's run (LUI-F1). Moving one
  to Steer or Interrupt now needs the wheel, as sending does; moving it to
  the queue does not.
- A frame's request with no Sec-Fetch-Dest (an older browser, a proxy that
  drops it) got no bridge, so a page could say hello itself and send
  follow-ups with no tap (LUI-F2). Now only a tab of its own (document) and a
  script's fetch (empty) get the page as written; a public link never gets
  the bridge.
- features.md: subagents' reports stay queued across an Interrupt.

Tested: npm test, 518 of 518.
@TannerMidd TannerMidd changed the title Pi Pocket 0.20.0 Pi Pocket 0.20.0: theme fonts, public artifact links, inline cards, and fast long sessions Oct 22, 2026
@TannerMidd
TannerMidd marked this pull request as ready for review October 22, 2026 23:16
@TannerMidd
TannerMidd merged commit 687a81d into main Oct 22, 2026
2 checks passed
@TannerMidd
TannerMidd deleted the release/0.20.0 branch October 22, 2026 23:24
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.

Opening a long session is slow and freezes the page

5 participants