Skip to content

release: 2.1.9 — typing freeze, full history, model picker, layout, settings - #404

Merged
0xSero merged 16 commits into
mainfrom
release/2.1.9-fixes
Oct 1, 2026
Merged

0xSero merged 16 commits into
mainfrom
release/2.1.9-fixes

Conversation

@0xSero

@0xSero 0xSero commented Oct 1, 2026

Copy link
Copy Markdown
Owner

Purpose

Ship the fixes for the reported problems: typing froze after a reply; history only showed for Local Studio; the model picker was a hard-to-read wall of 1,800+ entries; conversations sat off-center with code running off the screen; Settings pages were complicated. Replaces #403 (freeze fix) and #401 (version bump).

Key changes

  • Typing freeze (iOS): each keystroke re-rendered the whole conversation. Over 104 keystrokes after a finished Claude turn, ConversationView body went 81 → 1 and MessageList 268 → 4. The draft now lives in ConversationComposerDraft, bound only in a leaf (b898c33). Android was checked and was not affected.
  • History for every harness (Rust, both platforms):
    • Turn-pagination capability is tracked per (server, runtime) instead of per server.
    • The Claude and Pi bridges truncate turn lists without a cursor. The app now detects a full page with no cursor, shows it at once, then fetches the full list.
    • A Pi thread whose cwd was deleted opens with a 'History unavailable' row.
    • Claude Code wrapper markup (<local-command-caveat>, command tags, system reminders) is stripped from titles in shared Rust.
  • Model picker (iOS + Android): pick a harness, then models grouped by provider. Current and recent picks sit on top; search covers everything; Amp modes and plugin modes have their own sections. The Rust catalog classifies each entry as a model, mode or plugin mode.
  • Layout: scrollTo(edge: .bottom) slid the inset transcript stack 20pt sideways on phones. Rows now sit in a centered fixed column, and code and tables scroll inside their own box.
  • Settings (iOS + Android): three groups (Connections, Interface, More) with an Advanced page. The sessions toolbar has room for the computer name.
  • 2.1.9 version bump and release notes.

Verification

  • cargo test -p codex-mobile-client --lib: 863 passed.
  • make ios-sim-fast: BUILD SUCCEEDED.
  • LitterTests: 265 passed.
  • Live UI tests against a real kittylitter host (iPhone 17 Pro simulator):
    • History opens for codex, claude, opencode, omp, amp and pi.
    • Typing after a Claude turn: 2.05 s for 104 chars, no full-screen re-renders.
    • Opening the long Codex session takes 1.8 s; scrolling up through its history doesn't hang.
  • Screenshots reviewed for the picker, layout (iPhone and iPad), settings and sessions.

Not verified locally

  • Android is not built on this machine; this PR's CI is the first Android compile of the picker and settings changes.
  • testConversationDisplayCollapsedModeKeepsCompletedDetailsCollapsed already fails on main.

Follow-ups (alleycat repo)

  • Claude and Pi bridges should return a nextCursor when they truncate.
  • Pi should fall back to persisted turns when the stored cwd is gone.
  • The Claude preview should skip isMeta entries.
  • Serving a 15 MB Claude transcript takes about 5 minutes; it needs profiling.

🤖 Generated with Claude Code

0xSero and others added 16 commits September 30, 2026 18:42
The composer draft was a property on ConversationScreenModel, bound with
$bindableScreenModel.composerInputText inside ConversationDestinationScreen's
body. Every keystroke re-ran that body, rebuilt ConversationView with new
closures, and re-rendered the transcript. Measured over 104 keystrokes
after a finished Claude turn in the simulator:

  ConversationView.body   81 -> 1
  MessageList.body       268 -> 4
  BottomChrome.body       81 -> 1
  pinned-diff rebuild     81 -> 1

The cost grows with the transcript, which is why typing froze once a turn
had added tool output and markdown.

- Move the draft text and attachments into ConversationComposerDraft, an
  observable passed down by reference. Only ComposerDraftBinder, a leaf in
  the bottom chrome, builds the bindings, so keystrokes invalidate the
  composer alone.
- ConversationPinnedContextStrip built the combined diff in init on every
  parent re-render although SwiftUI keeps only the first @State initial
  value; build it in onChange(initial: true) instead.
- Add live UI tests against a kittylitter host (skipped unless env is set):
  typing after a turn settles, a screenshot tour, and open-session timing.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude sessions whose first user entry is a <local-command-caveat> or a
slash-command wrapper showed that raw markup as the session title and
preview. Clean titles and previews once in the shared ThreadInfo
conversion (and for home-cache entries written by older builds), so both
platforms get displayable text without parsing tags themselves.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
One Kittylitter host serves codex next to bridged runtimes (claude, pi,
opencode, amp, ...) whose thread/turns/list behave differently, but the
paging capability was a single per-server flag, so one runtime's
behaviour changed how every other runtime loaded history. The Claude and
Pi bridges also cut a page to 'limit' without returning a cursor, so the
app showed only the newest 5 (Android) or 20 (iOS) turns with no way to
load earlier ones.

Record the capability per (server, runtime). When a first page is full,
has no cursor, and the runtime has never returned one, fetch the list
once without a limit; if it is longer, keep the full history and mark
that runtime as non-paging. Threads a runtime cannot replay (Pi refuses
sessions whose cwd was deleted) now open with metadata and a 'History
unavailable' row instead of failing.

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

The composer picker showed one flat list (capped at 80 rows) of a
1,800-entry catalog with provider pills, Amp modes mixed in as models,
and effort chips plus Plan/Fast/Access toggles jammed underneath.

Rust now classifies every catalog entry when it is fetched:
ModelInfo.entry_kind (Model / Mode / PluginMode, from the agent's
visible_modes allowlist), picker_name (mode name, or the model name
without its provider prefix) and provider_label ("OpenAI", "xAI").
Both platforms read those fields instead of re-deriving them per row.

iOS (ModelPickerView) and Android (ModelSelectorPanel) now share one
layout: current selection and recents on top, one row per harness with
counts, a harness page with modes / plugin modes / models grouped by
provider (large catalogs fold, selected provider open), whole-catalog
token search capped at 150 shown rows, and an Options section with
reasoning, fast, plan and full access as list rows.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A non-paging bridge can take minutes to return a large transcript (a
100 MB Claude session takes 4-5 minutes on the host). Apply the first
page as soon as it arrives, then merge the full list when it lands, so
the conversation is readable while the rest of the history loads.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Scroll (and tap Load more) until the requested row appears, and allow a
longer hold via LITTER_E2E_HOLD_SECS for transcripts that take minutes
to arrive.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
On a phone the open conversation drifted left by the page margin once
it scrolled to the bottom: prose, bullets and code ran into the left
edge while the right side kept a 40pt gap. The transcript stack was
inset with horizontal padding and marked as the scroll target layout,
and `ScrollPosition.scrollTo(edge: .bottom)` aligns that layout's
leading edge, so following the bottom set a 20pt horizontal content
offset on a vertical-only scroll view.

The scroll target layout now spans the full viewport and every row sits
in a centered column of an exact width (screen minus margins, capped at
the 760pt reading width). A row with a wider ideal size (long code
line, table) can no longer widen the column; it scrolls inside its own
box.

Adds a rich transcript fixture (code block, table, diff, command,
reasoning) to the multi-turn harness and a UI test that checks both
gutters at the bottom and after scrolling, plus a settings screenshot
test.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Settings had nine categories, and four of them (Appearance, Pets,
Experimental, Support) opened a page holding a single link to the real
page, so most options sat three levels deep.

iOS root is now Connections (Computers with a count, Harnesses,
Account with the signed-in state), Interface (Appearance, Conversation)
and More (Advanced, Tip the Kitty), with the app version underneath.
Every row opens its settings directly. Rarely used switches live on one
Advanced page: experimental features, debug mode, Wake Pet and the
Local Studio link. The font family picker moved into Appearance next to
text size. Rows share one shape (icon, title, trailing value) and
sentence-case section headers; computer rows and the editor use
LitterFont tokens instead of fixed system sizes.

Android gets the same grouping: computers and connections first, font
family inside Appearance, and an Advanced screen with experimental
features, Wake Pet, Saved Apps, Local Studio and a Debug mode switch
(debug mode was previously not reachable from Android settings).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The sessions screen carried four toolbar buttons, which squeezed the
computer name to "macboo…". Refresh is now pull-to-refresh (and in the
filter menu), and server info moved into the same menu, leaving filter
and new session. The search field gains a magnifying glass and a clear
button.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
With LITTER_E2E_ATTACH the test activates an app already started by
'simctl launch --console-pty', so the Rust stderr log of the run can be
captured alongside the screenshots.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The sync is removed before release: iCloud KVS is not entitled in the
shipping app yet.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@0xSero
0xSero merged commit 366aefd into main Oct 1, 2026
10 checks passed
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