Skip to content

lody: mount the real mobile branch, scrubbed to the v1 scope — seam patch 16 - #171

Merged
pythonlearner1025 merged 10 commits into
mainfrom
lody-mobile-mount
Sep 2, 2026
Merged

lody: mount the real mobile branch, scrubbed to the v1 scope — seam patch 16#171
pythonlearner1025 merged 10 commits into
mainfrom
lody-mobile-mount

Conversation

@pythonlearner1025

@pythonlearner1025 pythonlearner1025 commented Sep 2, 2026

Copy link
Copy Markdown
Member

The diagnosis: the test, not the poller

packages/webapp/test/shell-gateway-resilience.test.tsx >
the platform snapshot poller >
never has two reads in flight against a box that does not answer
timed out at 5 s on clean main (3881574).

The poller is correct. useLodyPlatformSnapshot in
packages/webapp/src/lody/platform.tsx has not changed since #155 landed it.
git log names one commit for that file: 6a6cb18d (#155). None of #156-#164,
#172 or the cgroup changes touched it. The single-flight invariant holds. I
measured it: with a fetch that never answers, the hook issues exactly one
read, and 30 000 ms of fake time add none.

The test's timed region was wrong. The test imported
../src/lody/platform.js inside its own it() body. That module statically
pulls convex/react, @lody/platform and two @lody/components providers, so
the import is seconds of transform. Instrumented on this box with a real clock:

start:                    +0ms
A fake timers on:         +9ms
B imported platform.js:   +7720ms      <- the whole budget, spent here
C rendered reads=1:       +7765ms
D advanced reads=1:       +7766ms      <- the property itself: 1ms

The 5 s test timeout fired on module weight. The property was never reached.
The other ten tests in the file pass because their heavy import
(SessionSurface) is lazy() and mocked away.

The fix

The import moves to the file's static import block. The transform then lands in
the file's import phase, which no test timeout governs.

The assertion is unchanged. expect(reads).toBe(1) after
advanceTimersByTimeAsync(30_000) against a hanging fetch still states the
single-flight invariant, word for word. Nothing is relaxed and no test is
removed. The test now takes 27 ms instead of timing out at 5 018 ms.

Gates

Gate Result
npm run typecheck pass
npm run lint:gate pass — 74 anti-slop findings, all at baseline; 0 blitz-house
BLITZDEV_MANAGED=1 npm test — webapp 97 files, 857 passed, 6 skipped, 0 failed
BLITZDEV_MANAGED=1 npm test — box guest-tests 17 files, 106 passed, 0 failed
control-plane vitest 66 files, 723 passed, 0 failed
control-plane test:python / test:bash / test:host / test:scripts 1 / 4 / 13 / 59 pass, 0 fail

The control-plane stages were rerun alone: the box was running another agent's
full webapp suite at the same time, and the first combined run failed under that
contention. Every stage passes on its own with zero assertion failures.

The target suite was also run five times in a row after the fix: 11 passed,
five times out of five.

The 463-row support matrix and the user's KEEP/KILL/HIDE decision lived only
outside the tree, so no reader could check a claim against the code. This is
that record: the 25 areas and the call on each, the five answered DECIDE
questions, the five v1-scope flags and what carries each one, the seam-patch
index, and a dated amendment log.

Two amendments land with it. Mobile moves from KILL to IN — the branch this
commit opens mounts Lody's real phone experience. Connection status is
Lody-side removed, because BlitzOS chrome owns it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhdxNAYjV97JTv2pddcJiN
The vendoring rule is declare-then-edit, so this commit is the declaration
alone. Seam patch 5 wrote down that the mobile branch was deliberately left
unpatched, which was correct while both routes dropped it. Mounting the branch
makes that the gap.

Two halves, and one sentence explains both. session-detail.tsx returns for
mobile 952 lines above getSharedChatSurfaceProps, so props are lost at the
mobile fork and capabilities are not.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhdxNAYjV97JTv2pddcJiN
A phone got the desktop pages with their own mobile layouts inside, and none
of the shape upstream designed around them: the landing was torn down when a
session opened, there was no drawer to swipe back through, and no layer beneath
to reveal. Upstream owns that shape in MobileWorkspaceStack, mounted from
MainLayout, which BlitzOS does not mount and which takes one prop.

MobileSessionStack.tsx is that stack composed on our side, with the seven props
the two pages need: the landing stays mounted as the base, the session layers
over it in the same Vaul right-drawer the PR view uses, the last session stays
sticky so it can animate out, and close navigates rather than pops history.

The routes now do what upstream's do: the _auth layout carries the stack on a
stack route, and both leaves return null on mobile while ChatRoute keeps
publishing the base context the stack reads.

The landing keeps host tabs' content on a phone and loses their strip: the
strip is the desktop tab affordance, and a second tab system above Lody's own
is what plans/LODY-TERMINAL-TABS.md exists to prevent.

Two v1-scope props join the five: hideSettingsEntry (the mobile home's gear,
the same species as the hint band's Go-to-settings) and hideConnectionStatus
behind a new connectionStatus flag, because BlitzOS chrome owns that answer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhdxNAYjV97JTv2pddcJiN
Half A makes a workspace terminal reachable. The mobile tab sheet keeps its own
kind enum, which seam patch 5 left alone on purpose; it gains 'custom' and an
icon slot, the same two things the desktop strip already had. session-detail
appends the host list to mobileViewers, routes the tap to onSurfaceTabSelect,
mounts each host tab's content on the mobile branch, and lets an active host tab
cover the conversation the way it does on a desktop. The sheet gets no close
verb: upstream's viewer rows have none, and the BlitzOS rail closes a terminal.

Half A also closes a hole. session-detail:4368 has written kind: v.type from a
ViewerTabItem since wave 3, and that union gained 'custom' while the mobile enum
did not follow.

Half B is the scrub. The mobile branch returns 952 lines above
getSharedChatSurfaceProps, so it carried readOnly and no hide* at all: the
notification prompt and the Agent Role picker rendered on a phone and not on a
desktop. Copy URL, Share with team and Change owner each get the term the
desktop menu already has, and so does the sharing visibility row.

Two surfaces have no desktop twin and so had no gate. The mobile header's
GitHub mark re-derives the repo instead of reading the capability-nulled value,
which is a bug rather than a BlitzOS opinion. The home header's connection pill
and the header's sync indicator answer hideConnectionStatus, because BlitzOS
chrome owns that answer.

On the landing: the settings gear, the Add-a-GitHub-repository row, the GitHub
projects sub-tab and the download-the-client takeover. hideProductHints reached
only the desktop hint band before this.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhdxNAYjV97JTv2pddcJiN
…chine

lody-mobile-mount.test.tsx mounts the real vendored components and drives them.
A host tab is listed in the mobile tab sheet, carries the host's glyph and
reports its id; a host tab with no glyph renders the fallback, which is the row
that used to throw because the kind was not in the icon record. Every scrubbed
surface is asserted both ways — dark with the suppression and present without
it — because a test that only checks "the button is absent" also passes when
the component stopped rendering at all.

The source pin moved to lody-seam-pin.test.ts and now costs four readFileSync
calls. It was inside lody-surface-tabs.test.tsx, whose file-level beforeAll
imports Monaco, shiki, three and the Loro WASM; on a loaded machine that import
exceeds its hook budget and vitest reports every test in the file as skipped,
the pin included. A pin a slow machine silently turns off is not a pin. The
helper it shares with that suite is upstream-seam-pin.ts, and the two mobile
files seam patch 15 edits have baselines beside the two that already did.

The stack also forwards resetDraftKey, which upstream's own stack drops: the
BlitzOS rail's "New session" clears the landing draft through the address, and
without this a rail press on a phone would do nothing visible. It wraps the
pages in upstream's own container chain, because the mobile layouts size
themselves against it and BlitzOS mounts no MainLayout to supply it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhdxNAYjV97JTv2pddcJiN
If either `to` were not an address our tree holds, matchRoute would answer
false for the life of the surface, the stack would never mount, and a phone
would show a blank pane — a failure with no error in it. lody-router-targets
already proves both addresses exist; this proves we spell them the way
upstream's own mobile layout does.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhdxNAYjV97JTv2pddcJiN
It was compared to null and never read, so a find() built an object the code
did not use. some() says the same thing in one line and reads like the desktop
rule it mirrors.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhdxNAYjV97JTv2pddcJiN
Four assertions encoded the pre-amendment scope. Three were arithmetic — five
flags became six and seven suppression props became nine — and the fourth said
"the mobile branch is not mounted", which was true only because both routes
dropped it.

That describe is inverted rather than deleted. What the suite must hold now is
the other side of the same claim: the stack hangs off the route both leaves
share, both leaves return null on a phone and exactly two do, the chat leaf
still publishes the base context, and exactly ONE file in our tree imports a
vendored mobile screen. Two importers would mean two things draw the phone's
session, which is a duplicate mount rather than a feature.

The suppression check now reads both mounts. A prop may live in either —
hideSettingsEntry and hideConnectionStatus have no desktop surface at all — but
it must live in one of them, or the flag reaches nothing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhdxNAYjV97JTv2pddcJiN
#173 merged first and took the number 15 for "the host owns connectivity". This
is the rename, and it is more than a number: that patch and this one both had a
`connectionStatus` flag and a `hideConnectionStatus` prop, chosen independently
and spelled the same. Theirs is now the only one.

Deleted here as duplicates: the flag, the prop, both vendored declarations, the
chat-landing connectionUiState gate, and the mobile-header isSyncing gate —
their hunk 11 gates the flag at its source, above my call site.

Kept, and it is the one thing their patch could not reach: the mobile
SessionChatInterface now takes hideConnectionStatus. Their hunk 13 forwards it
through getSharedChatSurfaceProps, which session-detail defines 952 lines below
its own mobile return. That fork is the same sentence that explains why
hideCloudMenuItems, hideNotificationPrompt and hideAgentRoles needed forwarding
too. One flag, one prop, one gate story.

The pin file carries their new session-detail anchor beside mine, so both
patches are checked against one baseline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhdxNAYjV97JTv2pddcJiN
Seam patch 16's mobile stack imports getMobileMainLayoutRootClassName — two
class-name helpers MainLayout also calls, and that the stack reproduces
precisely BECAUSE it does not mount the layout that would supply them. A bare
substring check read that as a mount.

It now looks for an element or an import of the component. That is the claim the
test was always making, and the old spelling could have been satisfied by
deleting a doc comment.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhdxNAYjV97JTv2pddcJiN
@pythonlearner1025 pythonlearner1025 changed the title lody: mount the real mobile branch, scrubbed to the v1 scope lody: mount the real mobile branch, scrubbed to the v1 scope — seam patch 16 Sep 2, 2026
@pythonlearner1025
pythonlearner1025 merged commit e68e7a6 into main Sep 2, 2026
10 checks passed
pythonlearner1025 added a commit that referenced this pull request Sep 4, 2026
Brings the quick-action strip up to date with main after #159, #164,
#171, #173, #181, #183, #197 and #207. Thirteen files conflicted; the
resolutions, so a reviewer can check them one by one:

- `WebAppHeader.tsx`: deleted with the native tab strip on main (#159);
  the PR's edits to it are moot. Its two exports moved where main put
  them (`NewTabMenu`, `SessionTypeIcon`).
- `CloudApp.tsx`: main's strip deletion wins over the PR's drag wiring
  (`useWorkspaceTabDrag`, `moveTab`, `splitTab`, the drag ghost);
  the PR's side-panel binding, Connections host tab and mobile panel
  sheet stay; main's `workspace-chat-memory` stays. `filesHostRegion`,
  `ShareToDriveDialog` and `FolderAttachmentView` went with the Files
  pane the PR retires — their only callers were inside it.
- `router.tsx`: the PR's side-panel hooks sit above main's mobile early
  return, because hooks must.
- `SessionRailSidebar.tsx`, `SessionRail.tsx`: main's rows with the
  close button, minus the `filePath` the PR removed with file tabs.
- `lody-surface-tabs.test.tsx`: main moved the seam pin into
  `lody-seam-pin.test.ts`; the PR's anchors for the three new files and
  the four session-detail lines are ported there.
- `shell-smoke.test.tsx`: main's "the split is a placement" assertions,
  driven by the Connections icon the PR's strip draws; the drag test
  goes with the drag.
- `workspace-panes.test.ts`: `moveTab` is gone, so the backgrounded
  panel case builds its side-pane document by hand.
- `SessionTypeIcon.tsx`: `WebAppTabModel.panel` narrows to
  `'connections'`, which is the one panel the PR leaves.

Main already used seam numbers 10 and 11 for other patches, so the PR's
two seams are 19 and 20 everywhere: the ledger, the seam-pin test, the
baselines README, the upstream PR drafts and every code comment. The
merge runbook's file table keeps the PR's rows with the new numbers and
now says plainly that the ledger is the authority for the count.

Gates on the merged tree: webapp typecheck, lint gate, and the full
webapp vitest run (113 files, 948 tests) pass.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants