mirror of
https://github.com/stablyai/orca.git
synced 2026-09-21 16:02:20 +00:00
middle-click-issues
11403
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
fa306937f7 |
fix(terminal): address review nits on the #21762 middle-click fix
- Fix a mis-attributing comment: the suppression window (not preventDefault, which only helps on mousedown while Chromium's native paste fires on mouseup) is what swallows the duplicate native paste. - Drop the now-unused getPrimarySelectionMiddleClickPane. - Assert stopPropagation is/isn't called per tracking mode in the repro test. |
||
|
|
8309c53aeb |
fix(terminal): arm native-paste suppression on middle-click in mouse-tracking TUIs (#21762)
Orca's own middle-click paste path bailed out entirely whenever the pane was in mouse tracking mode (Claude Code, Codex, ...), skipping preventDefault() and never arming the #8993 native-paste suppression window. Chromium's native Linux middle-click paste then landed unsuppressed alongside the TUI's own PRIMARY paste from the forwarded mouse report, pasting the selection twice. Split pane lookup from the tracking-mode gate: any terminal pane target now arms suppression and blocks the native paste, while only the paste-to-PTY (and the propagation stop that would swallow the click before xterm can report it) stays gated on mouseTrackingMode === 'none'. |
||
|
|
fa4ea57871 |
fix(terminal): keep Pi input visible in open synchronized frames (#21708)
* fix(terminal): keep Pi input visible in open synchronized frames * test(terminal): keep synchronized input fixture lint-clean Place the existing SAFETY lint directive directly on the private xterm state assertion so the repository quality gate recognizes the reviewed test-only cast. * fix(terminal): preserve startup parse callback * fix(terminal): bound frame close after safety flush * test(terminal): type startup callback fixture |
||
|
|
ee61e3bd41 |
fix(mobile): measure the keyboard from visualViewport inside the page (OTA phase C, C4.2) (#21735)
* feat(mobile): measure the keyboard from visualViewport inside the page (OTA phase C, C4.2) react-native-web's `Keyboard` is a stub: `addListener` returns a subscription that never fires and `isVisible()` is always false. A screen inside the shell's page that waits for `keyboardDidShow` waits for the life of the document, and the software keyboard covers whatever sits at the bottom of it. Two C4 screens are text entry at the bottom. `platform/keyboard-occlusion` is the pair. The native file carries the source-control hook's logic unchanged, events and clamp and the comment that travels with it. The web sibling reads `visualViewport`: the layout viewport keeps its size and the visual one shrinks, so the occluded strip is `innerHeight - (height + offsetTop)`. `offsetTop` is in it because a scrolled or pinched visual viewport sits partway down the layout viewport and the strip below it is not keyboard; dropping the term reds two cases. It listens on `resize` and `scroll` — the browser scrolling a focused input into view moves the offset without resizing anything — and reads once at mount, because a composer opened over an already-raised keyboard receives no event at all; dropping that read reds a third case. `useKeyboardAvoidingPadding` is a second name rather than a `Platform.OS` branch at the call site. Natively it is 0 and subscribes to nothing, so a composer that asks for it renders exactly as often as it does today; `KeyboardAvoidingView` has already moved it and padding would move it twice. On the web it is the whole of the avoidance, that view being driven by the events this file exists because the page never receives. No `visualViewport` answers 0 rather than guessing. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): lift the commit bar and the note composer inside the page (OTA phase C, C4.2) The two consumers move onto the seam. The hub's hook becomes one line and keeps its name, which is what the hub's state calls the number. The note composer takes the padding as a style on the `KeyboardAvoidingView` it already had: natively that is 0, so the prop is `undefined` and the phone renders exactly what it rendered before; inside the page it is the strip the keyboard covers, which is the only thing that moves the composer there. The census is over both future route closures rather than over the two call sites: `platform/keyboard-occlusion` is the one module in either closure allowed to name the stub. Red first at the base commit — run in a throwaway worktree at `9309350864` rather than by setting the fix aside — it named `use-mobile-source-control-keyboard-lift.ts` as a subscriber outside the seam and found the seam's web file in neither closure. `mounted-bottom-drawer.tsx` is exempt by name, and the census asserts the exemption is really in both closures so it cannot outlive its subject. It reads more than a height — `Keyboard.metrics()` for a sheet opened over a raised keyboard, and each event's `duration` to animate with it — which the seam does not model, and it sits in C1's, C2's, C3's and C5's closures too, so moving it is a change to every page rather than to this domain. Its listeners are inert on the web the same way, which is why the composer inside it takes its own padding rather than inheriting one. No render-check case: measured, none of the five registered routes reaches the seam, the commit bar or the composer, and a headless browser cannot shrink the visual viewport independently of the layout one anyway. C4.4 carries it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): type the keyboard harness instead of asserting its fields (OTA phase C, C4.2) The changed-code gate flagged the two `as` casts in the hoisted harness. A return type on the `vi.hoisted` callback says the same thing and is checked rather than asserted, which is the shape the host-list route test already uses. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): read a pinch zoom as no keyboard, and test the clamp (OTA phase C, C4.2 round 1) Round-1 folds plus CodeRabbit's exemption point. **A pinch zoom read as a keyboard.** A 2x zoom shrinks the visual viewport by exactly as much as a half-screen keyboard, so the commit bar and the composer moved on a page nobody was typing into. A `scale` other than 1 answers 0. Geometry alone cannot tell the two apart and a stored "no keyboard" baseline would be a heuristic, so a keyboard raised while zoomed is the accepted rare case rather than a guess. `scale` is read defensively because older WebViews do not implement it, and taking its absence for zoomed would answer 0 for every keyboard on them; mutating the guard to key on absence reds both cases. **The clamp had no test.** A bare subtraction left all nine cases green. The case is a visual viewport taller than the layout one, which mobile Safari reports mid-scroll and which would have pushed the commit bar down the screen instead of up. **One guard, where the test reaches it.** `occlusion`'s `viewport === undefined` arm was unreachable: the effect returns before calling it, and the absence case exercised that one. Deleted, and the remaining case says which guard it proves. **The census exempts two files, not a directory.** `startsWith('src/platform/')` would wave through a later `src/platform/*.web.ts` that subscribed to the stub directly, which is the defect this census exists for. Named exactly, with a planted subscriber beside the seam as the fixture; restoring the directory filter reds it. **And the moved comment claimed an inset it never subtracted.** Deleted. Correcting a comment that was false where it came from is not a rewrite of the logic the move carried: no statement moved with it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): keep the page at scale 1 so the zoom guard is not the keyboard path (OTA phase C, C4.2 round 2) Round 2's finding changes what the zoom guard costs. iOS auto-zooms on focus of any input under 16px; both consumers' inputs are 14px (`typography.bodySize`), and the page's viewport meta set no `maximum-scale`. So `scale !== 1` was not the rare pinch the guard was written for, it was every focus — and the seam would have answered 0 on the one flow it exists for. The guard stays and the premise is fixed instead: `maximum-scale=1` in both places the page's meta is written, the built document in `build-mobile-web-app-bundle.mjs` and the bootstrap `index.html`. iOS honours it for the focus auto-zoom and has ignored `user-scalable=no` since 10, so a deliberate pinch still works; the input sizes are untouched. C4.6 step i is what settles it on a device. Three test changes and one correction. The census took a `rootDir`, as `findWebSiblings` does: it planted `src/platform/other.web.ts` in the real tree while the overrides census walks `mobile/src` in a parallel worker and would read it as an unlisted override. It plants under `mkdtemp` now, and writes the two seam files there too, so the empty result for them is the name exemption working rather than those files happening not to subscribe. A case for the ruling itself: scale 2 with a viewport shrunk past what the zoom explains answers 0. Dropping the guard reds it and the pinch case together. `useKeyboardAvoidingPadding` is rendered through the test renderer now instead of called outside one, with a counter on `Keyboard.addListener`. Making the native hook return `useKeyboardOcclusion()` reds it at two calls; the old shape could not see that, because a hook read outside a component never runs its effects. Item 4 did not hold as written. `window.visualViewport ?? undefined` is not a no-op: the DOM declares the property `VisualViewport | null` and an older WebView omits it entirely, so the coalesce was normalising both shapes into one `=== undefined` check. Removing it and testing only for `null` throws on the absent-viewport case (reproduced: `Cannot read properties of undefined (reading 'scale')`). The coalesce is gone and the guard names both shapes instead. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): raise the two page inputs to 16px on web instead of pinning the page scale (OTA phase C, C4.2 round 2) `maximum-scale=1` is reverted from both metas. It fixed the right problem in the wrong place: Android WebView honours it and iOS ignores it for pinch, so the cost of stopping an iOS focus auto-zoom was deliberate zoom on Android, taken from the users who need it most. The font size is where it belongs. `src/platform/text-input-font-size.ts` is the app's body size and `.web.ts` is that raised to 16, the size below which iOS zooms on focus and does not zoom back. The commit bar and the review note composer take their `fontSize` from it. A phone renders what it rendered before: the native constant is `typography.bodySize`, so both style objects are unchanged there. `Math.max` rather than the literal, so a theme that raises the body size past 16 keeps its own value. The zoom guard stays and its rationale is rewritten to say what now keeps the ordinary path off it: the inputs clear the floor, so a scale other than 1 means a user pinched rather than an input took focus. The pin is a unit case because the render check has no route to open yet. Three assertions and what reds each: the web constant below 16 reds the first, and a style going back to `typography.bodySize` reds the third, which reads the two stylesheets as source because a node test resolves the native sibling and would otherwise pass while shipping 14px to the web. The overrides census covers the swap itself. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): put every text input in the two closures on the size seam (OTA phase C, C4.2 round 2) The 16px floor reached two inputs and the rationale claimed a page. Eight more text inputs in the same two closures still declared 14px, so a focus on any of them zoomed the document and the occlusion seam — which reads a scale other than 1 as no keyboard — stopped lifting for the rest of that session. "A scale other than 1 means a pinch" was false while they were there. All eight go through `TEXT_INPUT_FONT_SIZE`, named by the census before the change: src/components/MobileSearchField.tsx:175 src/components/SmartWorkspaceAdvancedFields.tsx:84 src/components/SmartWorkspaceSourceField.tsx:137 src/components/new-worktree-form-styles.ts:125 src/components/pr-sidebar/MobileLinkPrForm.tsx:120 src/components/pr-sidebar/mobile-pr-sidebar-styles.ts:299 src/components/pr-sidebar/pr-comment-composer-styles.ts:20 src/components/smart-workspace-source-drawer-styles.ts:60 Every one declared `typography.bodySize`, so there was no input carrying a size of its own to preserve and the phone is byte-identical again. Each of those style keys was checked for consumers first: all of them are read by a `TextInput` and nothing else, so raising the web value moves no other element. The census is the rule rather than the list. Over both closures it resolves each `TextInput`'s style to the module that really declares the size — following a spread, because both seam-served inputs are reached through `{ ...base, ...list }` and a walk that stopped at the first module would have called their offence absent — and names anything not on the seam as `path:line`. A style with no `fontSize` inherits and is not an offender. Presence precondition: the seam's web file is in the closure, so an empty list cannot mean a page with no inputs. Run against the previous head it prints exactly those eight for both routes; three fixtures under mkdtemp cover the cross-module line, the spread, and the two non-offender shapes. The web test's rationale named `maximum-scale=1`, which is gone; it names the input floor now. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): make the input census prove its own enumeration (OTA phase C, C4.2 round 2 addendum) The offender list only says every text input is on the seam if every text input was read, and the walk could not tell "this key sets no size" from "I could not follow this style" — both answered nothing, so a resolution failure would have read as a clean input and the rule would have gone quietly vacuous. `resolveStyleKey` answers three ways now: not found, found with no size, found with one. `unresolvedTextInputStyles` reports the first as `path:line (key)`, and the census asserts it is empty for both closures beside asserting the offender list is. Measured rather than assumed, which is what the addendum asks for. The two closures hold 12 `TextInput` elements and 13 style references; none uses an inline style object and none is without a style prop. All 13 resolve, 12 to `TEXT_INPUT_FONT_SIZE` and one — `styles.disabled`, combined with `styles.input` on the same input — to a style that really sets no size. The reviewer picker is in that list at `mobile-pr-sidebar-styles.ts:300`; it was already on the seam from the previous commit, which enumerated from the closure rather than from the review. A fourth fixture plants both shapes side by side: a style with no size, which is not an offender, and a style reached through a package import, which is named. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): close three holes in the text-input census (OTA phase C, C4.2 fold 3) All three of CodeRabbit's findings are on the completeness property the addendum bought, and all three reproduced before the change: each shape below answered 0 offenders and 0 unresolved, which is to say it vanished. Inline style literals. The walk recorded only `object.key` references, so `style={{ fontSize: 14 }}` was neither an offender nor a hole. Style props are flattened structurally now — arrays, spreads, `?:`, `&&` and parentheses down to the expressions that can really land — rather than walked as a subtree, which had the second bug of descending into an inline literal's own properties. `&&` is followed because `[styles.input, disabled && styles.disabled]` is the shape this tree actually uses; `null`, `undefined` and `false` branches contribute no style and are dropped rather than called unfollowable. An inline literal resolves in place, and any other shape — a call, a bare identifier — lands in the unresolved list. Source-order precedence. `{ input: safe, ...legacy }` is `legacy.input` at runtime, and answering direct keys before spreads read `safe` and called the override clean. Properties are walked in reverse source order now, direct keys and spreads in one pass, first answer wins. The seam by binding. `size.text !== SEAM_EXPORT` accepted anything spelled `TEXT_INPUT_FONT_SIZE`, so a local `const TEXT_INPUT_FONT_SIZE = 14` two lines up passed, and so did an import of that name from any other module — the regression the seam exists to stop, wearing its name. The identifier is resolved in the declaring module and accepted only as an import from `src/platform/text-input-font-size`. That last one changes what a fixture must say: the existing seam case spelled the name without importing it, so it plants the seam module and imports from it now. Six new fixtures, all six red on the previous walk. Re-measured at this head, both closures: 12 `TextInput` elements, 13 style references, 12 on the seam, 1 sizeless (`styles.disabled`, combined with `styles.input` on one element), 0 offenders, 0 unresolved. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
e225b4b7eb |
Fix stale Codex usage after reset (#21748)
* fix(rate-limits): refresh Codex usage after reset * fix(rate-limits): converge weekly Codex reset usage --------- Co-authored-by: m4air <m4air@Mac.localdomain> |
||
|
|
b5b727bddb |
feat(composer): restore compact branch picker UX (#21741)
* feat(composer): restore compact branch picker UX * fix(composer): address picker review feedback |
||
|
|
f3bda1bf3e |
refactor(mobile): seam moves and the shared shell route guard for the source-control domain (OTA phase C, C4.1) (#21732)
* refactor(mobile): open PR sidebar URLs through the external-link seam (OTA phase C, C4.1) The three openers in the PR sidebar called `Linking.openURL` directly: a check's "open on the web", a comment's permalink, and a link inside comment Markdown. Inside the shell's WebView react-native-web routes that to `window.open(url, '_blank', 'noopener')`, which both shells refuse and which resolves anyway, so the tap reports success and opens nothing. Both C4 routes reach the sidebar, so both would have shipped that. The census is the point rather than the three edits. It derives the two future route closures through `mobileWebAppRouteClosure` and holds every module in them to the seam, so a module entering either closure later is ruled without anyone adding it here. Red first it named all three by `path:line`: CommentMarkdown.tsx:2, PRChecksSection.tsx:2, PRCommentCard.tsx:2, on both routes. The walk it runs was the third copy of one function, so it moves into the seam's own module beside the predicate that module exists to share, and the files and tasks censuses now call it too. It reports `path:line` where the copies reported paths; `reachesReactNativeLinking` keeps its name and its meaning and is now derived from the line list, so there is one rule. An empty offender list is empty in either shape, which is why repointing the two landed censuses moves nothing they assert. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): copy through the platform clipboard seam in review and conflicts (OTA phase C, C4.1) The two copy actions both C4 routes reach called `expo-clipboard` directly: the conflict section's refresh commands and the review sheet's notes. On the web that module is `navigator.clipboard`, which needs a secure context — the iOS shell serves the page from a custom scheme and Android from https, so the path works on one platform and silently not on the other. `useClipboardWriter` is the seam C2.4 landed for exactly that. Red first, the census named both routes: `ExpoClipboard.web.js` in each closure, and `src/platform/clipboard.web.ts` in neither. Both call sites also stopped ignoring whether the pasteboard took the text. The conflict section already returned on a throw, so the seam's rejection reaches an arm it had. `copyNotes` had none and its only caller is `void controller.copyNotes()`, so a rejection would have been unhandled with "Review notes copied" left on screen; it now catches and reports through the screen's own error line. That is the one behaviour change here and the reason `clipboard` joins its dependency array. Its suite mocked `setStringAsync` as resolving `undefined`, which the seam reads as a pasteboard that refused, so every copy would have gone down the new refusal arm unseen. The mock now resolves `true` and two cases pin both arms; mutating the catch away kills the refusal one. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): take the source-control router from the handoff seam (OTA phase C, C4.1) The hub takes its router once, in the openers hook, and passes it down to the runners and the panel — so one `useRouter()` is this domain's whole reach into routing, and it was expo-router's own. Inside the shell's page that posts no `navigate`, so the hub's push to review would stay in the document whatever its grants, and its push to a native route would paint Unmatched over the page. Inert today: no C4 route is registered yet. `use-mobile-source-control-runners.ts` is the second case and the reason the rule reads value imports rather than identifiers: it named expo-router only to write `ReturnType<typeof useRouter>`, a value import in a type position that keeps the module in the graph. `RouteHandoff` is the seam's own name for that type. The census is C3.1's, and its walk moves to `src/navigation` rather than being copied a second time; each domain keeps only its own evidence, the list of modules meant to hold a router. Red first it named both modules on the expo-router rule and reported no handoff caller at all. The C2.9 hop census is unchanged and cannot move: its targets come from the call sites, and the derivation over this tree returns the same ten targets and the same 26 unresolved sites before and after this commit, byte for byte. Its `HANDED_OFF` pin is over registered routes, of which this adds none. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * refactor(mobile): move the shell route guard out of the files domain (OTA phase C, C4.1) `src/files/mobile-file-shell-route.ts` was never about files: it parses a route against `BridgeInitRouteSchema` and builds the key a shell screen remounts on. It moves to `src/mobile-web-shell/shell-screen-route.ts` as `shellScreenRoute` / `shellScreenRouteKey`, with its test. The move is pure — with the rename applied and comments stripped, the old file and the new one diff to nothing. Three routes had grown their own copy of the call and two had none. The copies go: `agent-history` and `tasks` now ask the shared predicate, which is the same schema and the same fallback they already had. `index.tsx` had no guard at all, so a `.` or `..` host id was handed over and came back as "Update Orca to open this workspace" painted over the native list behind the switch; it now stays native. That is the one behaviour change here, pinned red first and killed by mutation. `web.tsx` keeps handing that route over on purpose and is exempt by name: its fallback is a redirect to the route the user came from, so the host's own verdict is the better answer there, which `mobile-web-shell-route.test.tsx` already pins. No `key=` expression moved; the three switches still key differently (host id, pathname, pathname plus params) and making them agree is a behaviour change for another PR. The census walks the route tree rather than a list, so a switch added later is held to both rules without being added here. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): mount the copy cases without a client instead of casting one (OTA phase C, C4.1) The changed-code gate flagged four type assertions on the two cases added with the clipboard seam: they stubbed an `RpcClient` the way the file's older cases do, and the gate reads changed lines. Copying reaches no client at all, so they mount without one, which is both cast-free and a truer statement of what the path needs. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): say what the censuses report and sort the red list by line (OTA phase C, C4.1 round 1) Round-1 folds, four wordings and one ordering. `externalLinkOffenders` said "every call site" and reports the line the name enters the module: a named import once, however many times the module calls `openURL`, because the import is what the rule is about and what has to go. Only a namespace import reports its uses, there being no single line to name. The docstring now says that. Its red list sorted the rendered strings, which puts `:10` before `:2`. It now sorts by path and then by line as a number. Pinned against a written fixture rather than the tree, because the case needs a module with sites either side of line ten and no module in a closure has to keep having one — the first fixture used lines 11 and 12, where both orders agree, and the mutation walked straight through it. `shell-screen-route.test.ts` still named the files screens in its describe after the guard stopped being theirs; it names what a switch does now. `router-seam-census.test-support.ts` excluded `.test-support.ts` from the walk, which the files census it was extracted from never did. Dropped, so both censuses walk the same set. Inert today: neither `src/files` nor `src/source-control` holds such a file, so it only decides the next one. The `web.tsx` exemption claimed a redirect "that looks like nothing happened". What was measured: adopting the guard there sends a `..` deep link through `Redirect href="/h/.."` to the host route, which this PR keeps native, so the developer lands on the host list with nothing said about why the page did not open. The route is `__DEV__`-only and the host's own failure screen is the better verdict. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): see every react-native alias, normalise the host id, surface a refused copy (OTA phase C, C4.1 CodeRabbit) Three bots findings on #21732, all real. **Every alias, not the first.** `reactNativeLinkingSites` found namespace imports with `exec` and inspected only the first binding, so a module importing the namespace twice and calling `Linking.openURL` on the second reported no site at all. It reads every alias now and counts a line once however many meet on it. Red first with exactly that fixture. **The host id can be an array.** `app/h/[hostId]/index.tsx` read it bare, and Expo Router answers a repeated key with one: `String(['a','b'])` is `a,b`, `encodeURIComponent` makes that the single segment `a%2Cb`, and the segment rule accepts it — so the shell opened a page for a host nobody has. Through `firstParam`, as the other four switches do. Red first it handed over `/h/host-1%2Chost-2`, and the empty-array case found a second one: `[]` is truthy, so a bare read built `/h/` and handed that over too; `firstParam` answers `''` and the route stays native. That import pulls the source-control screen state, and with it the lucide barrel whose `LucideProvider` re-export is the gap the web build patches, so the suite mocks the barrel as the other suites do. It moves no page closure: the closure resolves `index.web.tsx`, which this does not touch, and the index route still measures 3426 modules, 289 local, 22 families. **A refused copy said nothing.** `PRConflictingFilesSection` caught the rejection and returned: no tick, no message, a tap indistinguishable from one that copied. The label now carries the third state, reusing the tasks page's own wording for it, and the component has its first test. Mutating the failure arm away reds it. Its prop narrows to `Pick<PRInfo, 'mergeable' | 'conflictSummary'>`, which is what it reads and what let the test drop a cast the gate flagged; every caller holds a full `PRInfo` and satisfies it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): read the censuses' subjects as code, not as text (OTA phase C, C4.1 round 2) Round-2 additions. A separate commit because `d4b14d54e5` was already made and this lane does not amend. **The seam walk parses now.** Matching `X.Linking` in the text named it inside a comment that talks about it and inside a string that quotes it, and the named-import regex did the same for a commented-out import. Checked against the previous implementation, all three fixtures were red there: the comment case reported lines 2 and 3, the string case reported the string's line beside the real call, and `// import { Linking } from 'react-native'` reported line 1. The walk builds a `SourceFile` and reads import declarations and property accesses, so comments and strings are gone by construction and the quote styles stop being a special case. Cost measured on the three closure censuses: 3.3 s, unchanged. **The route census reads the call, not the import.** A switch that keeps the import while the call goes — deleted, or moved behind a branch that never runs — looked exactly like one that asks. It now needs both, proved by mutation: dropping `shellScreenRoute(` from `tasks.tsx` while leaving its import names `tasks.tsx`. A fixture carries the same rule in isolation, since every switch in the tree calls what it imports and the case would otherwise be unfalsifiable against it. **And recognises a switch by its import** of `MobileWebShellScreen` rather than by `<MobileWebShellScreen` in the text, so an alias or a line break the formatter chose cannot hide one and a comment cannot invent one. The `app/h/[hostId]` root stays written out: deriving it from the manifest is not a one-liner from here, the manifest being an `.mjs` this test reads as text. What ties the two together instead is a new case asserting every registered pathname starts with that prefix, so a page route outside it fails rather than going unwalked. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): read the imported name, not the local one (OTA phase C, C4.1 CodeRabbit) `import { Linking as NativeLinking } from 'react-native'` went through the census untouched: the walk compared the specifier's local binding, which is `NativeLinking`, while the imported name lives in `propertyName` when a specifier renames it and only in `name` when it does not. Reproduced before the fix — the aliased import with a call beside it reported no site at all. Reading `propertyName ?? name` closes it in both directions. A module that renames `Linking` is named at its import line like any other, and a module that imports `View as Linking` is no longer named for a local binding that reaches nothing. The second was a false positive the old comparison had by construction. One more of the same class, found while checking and verified rather than assumed: `import RN from 'react-native'` typechecks in this project (tsc accepts it), and a default binding is the whole namespace exactly as `* as RN` is, so `RN.Linking.openURL` through it was invisible too. The default binding joins the alias set, which already reports uses rather than the import. Four fixtures. Three red on the previous walk: the renamed import, the local-only `Linking`, and the default import. The fourth — an alias imported that never reaches `Linking` — passed before and is here to hold the other half of the rule, that importing react-native is not itself the offence. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): parse each module as its own kind, and read re-exports (OTA phase C, C4.1 CodeRabbit) Two ways a module reached `Linking` past the census, both reproduced before the change. Every file was parsed as TSX. In a `.ts` module `const id = <T>(value: T) => value` is a generic arrow; as TSX it is an unclosed JSX element, and the parser folds the rest of the file into the error node. A `RN.Linking.openURL` after one reported nothing, and so did the same call with its import above the arrow. The file name goes into the parse now and TypeScript reads the kind off the extension; `externalLinkOffenders` passes the real path, which it had all along. `ExportDeclaration` was never inspected, so `export { Linking } from 'react-native'` put the name back in reach of anything importing that module while the census saw an import list it was not on. All four shapes are read — named, renamed, `export *` and `export * as` — and reported at the export statement, which is the line to delete exactly as an import is. A re-export of another name, or of `Linking` from somewhere that is not react-native, stays unnamed. Seven fixtures. Five red on the previous walk: the `.ts` generic arrow and the four re-export shapes. The two that pass before and after hold the other half, that re-exporting is not itself the offence. The named-import and re-export clauses read `propertyName ?? name` through one helper rather than two spellings of it. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
84d827a6ab |
fix(daemon): pause producers when stream backlogs grow (#20947)
* fix(daemon): pause producers when stream backlogs grow * fix(daemon): reset stream backpressure on socket replacement * docs(daemon): point retention audit at current reproducer * test(daemon): validate stream retention audit outcomes * fix(daemon): bound the stream producer stall and leave a visible gap Stream backpressure pauses a session's PTY with no deadline: the only un-pause comes from the consumer draining, so a half-open peer that stops reading without closing freezes the shell for the rest of the session. Arm a 60s watchdog on the false->true stream-pause transition (not on the re-assertions refresh() makes for neighbouring sessions). On fire, mark the session stall-released: it becomes keep-tail droppable, its backlog is thinned behind a dataGap, and the producer runs again. The existing dataGap path makes the renderer restore that pane from the daemon's snapshot, so the user sees the terminal jump to current rather than sit frozen. The mark clears once the session's last byte leaves the daemon, restoring ordinary pausing. Nothing here reports a process exit - loss of contact with a consumer is not evidence about the child. Also enable TCP keepalive on the stream socket so a genuinely dead peer closes and onStreamDisconnected clears the pause. * test(daemon): put each casting SAFETY: directive on one line `oxlint-disable-next-line` covers only the line directly after it, so a rationale wrapped onto a second comment line suppressed nothing and the casts failed the changed-code quality gate. Drop the remaining JSON.parse cast for an annotated binding. --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: Neil <neil@stably.ai> |
||
|
|
921882619e |
fix: retire closed editor models from the app shell (#21178)
* fix: retire closed editor models from the app shell * test(editor): use checked Monaco attachment calls * Preserve bounded editor view caches when retiring closed models * docs(editor): describe batched model retirement * fix(editor): preserve cleanup work across registry replacement * fix(editor): build editor model URIs with the file scheme Monaco keys its model registry by `uri.toString()`, and both `@monaco-editor/react` (via the `path` prop) and the closed-tab disposal path built that key with `Uri.parse`. On Windows a raw path such as `C:\repo\a.ts` parses as scheme `c`, which fails the scheme gate in `modelService._schemaShouldMaintainUndoRedoElements`, so closed-file undo history was dropped for every file at any size — not only the large files the tradeoff note covers. Add `toEditorModelUri`, the one filesystem-path -> model-key function, built on `Uri.file` so the result always carries the `file:` scheme and re-parses to itself. Route model creation, disposal lookup and the still-open ownership comparison through it so all three agree; a divergence there would dispose a model an open editor is still editing. --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: Neil <neil@stably.ai> |
||
|
|
65cde9bb80 |
fix(mobile): name the source-control Back controls and split the dock's Close from Back (OTA phase C, C4.3) (#21739)
* fix(mobile): name the custom-key drawer's Back for the accessibility tree (OTA phase C, C4.3) `CustomKeyModal`'s Back is a bare `Pressable` with a label and no role, so a screen reader has nothing to announce it as. It is reachable today from the session sheets and the terminal shortcut settings, so this is a gap now, not only inside the page. It surfaces here because `screenTree` takes a screen's directory: C4.4 registering the review route puts the whole of `src/components` under the Back rule. Fixing it in the PR that registers would make a route entry carry unrelated accessibility work, so the census gains a case that holds the arriving trees to the same two rules before the rows land. Red first it printed exactly what the rule would: src/components/CustomKeyModal.tsx:191 role=none label=Back Once `PAGE_SERVED_SCREENS` has the two rows, `CONTROLS` covers these trees and the new case becomes a second reading of the same thing; it says so. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): split the source-control header's Close from its Back (OTA phase C, C4.3) One `Pressable` served both modes — `onPress={onBack}` with a conditional label, `X` docked and `ChevronLeft` full-screen. The Back census reads a control by what its handler does, so it sees that one as a Back and then requires a label starting with "Back", in a mode where the control dismisses the dock beside the terminal. The cheap way to go green is to call a close "Back", which satisfies the rule by making the wording wrong. So the modes become two controls. Embedded presses `onClose` and is named "Close source control"; otherwise it presses `onBack` and is named "Back to session". Both carry the button role. The panel stops choosing by mode and passes both handlers; the dock keeps exactly the behaviour it had, its dismiss still `onRequestClose` falling back to a pop. Probed before it was written: run through the census's own predicate, the post-split shape yields one back control rather than two — `onClose` matches neither the handler pattern nor a declaration this file holds, being a destructured prop — so the Close is invisible to the rule and the presence precondition still holds on the Back. The component test is what the census cannot do. Nothing else pins this: no golden names this component and no parity census covers `src/source-control`. Red first against the single control, both cases failed on the missing role. And renaming the Close to "Back to close source control" — the gaming this split exists to prevent — reds the component test while leaving the census green, which is the whole argument for having both. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): assert a Back per arriving tree, and name the refresh control (OTA phase C, C4.3 round 1) The arriving-trees presence case counted controls over the union of both trees, so one tree answered for the other. Per tree now, in the shape the block above it already uses per screen module. Red first, with the mutation round 1 named: rename the route branch's handler to `onDismiss` and its label to `Return to session`, and the only Back in `src/source-control` disappears. The per-tree case names that tree. The same mutation against the union count passes all six cases, which is what the change is for. The refresh control had a label and no role, so react-native-web renders a `div` carrying `aria-label` and a screen reader announces no control. Its two siblings in this header already carry one. Two claims in my round-1 report were wrong and I am the reason the body carried them. The Close pressing `onBack` reds the census's label rule as well as the component test, not the component test alone; and no fixture of the post-split shape exists — the shape is read from the real file. Both were stated from reasoning rather than from a run. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): assert an arriving Back per screen, not per tree (OTA phase C, C4.3 round 2) Round 1 moved the arriving-trees presence assertion from the union to each tree, which was not far enough. `src/components` holds two Backs, so the tree answers for both: renaming `MobileDiffReviewHeader`'s handler to `onDismiss` and its label to `Return to session` leaves every case in the file green, with `CustomKeyModal.tsx:191` standing in for the screen that just lost its Back. Reproduced before the change — six passed with the review header's Back gone. Per screen module now, which is what the table above already does and for the same reason its docstring gives: a directory with more than one control cannot say which one a rule was written about. The two modules are named and the trees derive from them, so the pair C4.4 adds to `PAGE_SERVED_SCREENS` is the same pair spelled once here. Both mutations red on the new case and name the file: the review header's rename names `src/components/MobileDiffReviewHeader.tsx`, and round 1's source-control rename still names its own, so this does not trade one cover for another. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb --------- Co-authored-by: Claude <noreply@anthropic.com> |
||
|
|
ee354a35d7 |
feat(agents): add OpenCode 2 beta support (#21418)
* feat(agents): add OpenCode 2 beta support Co-authored-by: Xiro The Dev <lethanhtrung.trungle@gmail.com> * fix(opencode2): support current plugin lifecycle and session storage * fix(opencode2): preserve lifecycle ordering and full session capture * test(opencode2): cover setup event bridge * test(opencode2): cover setup event bridge * test(browser): satisfy anti-slop naming check * test(opencode2): cover live form lifecycle * fix(relay): preserve OMP config directory selection * test(opencode2): avoid assertions in bridge fixture * fix(rebase): retain OMP resume and fresh launch behavior * test: align upstream OMP resume expectations * test(opencode2): verify rejected form closes waiting state --------- Co-authored-by: Xiro The Dev <lethanhtrung.trungle@gmail.com> |
||
|
|
403c0881e1 |
Bound AI Vault transcript record assembly before allocation (#20963)
* fix(ai-vault): bound incremental transcript record assembly * fix(ai-vault): skip one oversized record instead of dropping the session An agent transcript record over the 10 MiB budget threw out of the JSONL fold, so the whole session vanished from Agent Session History and from search. A 10 MiB base64 image or a runaway tool result is ordinary. The reader now discards the offending record up to its newline and keeps folding. The in-progress record always starts at `consumedThrough`, which is what makes both its running size and the resume offset past a discarded span exact; an unterminated oversized tail leaves the cursor at the record's start so a still-growing record is re-read rather than guessed at. Skips accumulate on the resume point keyed by start offset, and the scanner reports them as a per-session `notice` so nothing is silently lost. The budget itself is unchanged. --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: Neil <neil@stably.ai> |
||
|
|
f87359cda6 |
fix(runtime): persist acknowledged terminal tab retirement (#21020)
* fix(runtime): persist acknowledged terminal tab retirement
* test(runtime): drain tab retirement fixture writes before teardown
* fix(runtime): explain a refused workspace terminal close
The Sleep-workspace path threw the raw refusal enum ("stale-terminal") as an
Error message, which reaches a CLI user verbatim and a Sleep toast via
describeSleepFailure. Map each refusal reason to a sentence instead.
Also pins two behaviours that had no coverage: the user-visible outcome of a
republished stale-terminal refusal on the web client (the caller cannot tell it
from a real close), and the one-call-per-close invariant that keeps a successor
terminal alive.
The bounded close retry was NOT implemented: notifier.closeTerminalTab carries
only a tab id, so a second call destroys whatever successor took that id.
* test(runtime): build refusal fixtures without type assertions
The changed-code quality gate rejects new `as` casts. Replace the
branded-outcome cast with refusedMobileSessionTabClose, and model the
wire-skew reason as a decoded host answer instead of `as never`.
---------
Co-authored-by: m4air <m4air@Mac.localdomain>
Co-authored-by: Neil <neil@stably.ai>
|
||
|
|
db7b57b846 |
fix(claude): enforce history window quota while reading (#21021)
* fix(claude): enforce history window quota while reading * test: repair history quota audit dependency and CI import * fix(native-chat): record why restart reconciliation leaves work unconfirmed Two silent paths hid the cause of an unconfirmed submission. The reconciler's bare `continue` on an `unknown` outcome dropped the reason it already carried, and the transcript read swallowed its error, collapsing an oversize file and a genuine read failure into the same verdict. Log both. No control flow changes. --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: Neil <neil@stably.ai> |
||
|
|
a445abadd4 |
fix(browser): bound CDP output for stalled clients (#20949)
* fix(browser): bound CDP output for stalled clients * fix(browser): log CDP outbound overflow before terminating the client The outbound queue terminated the automation client silently on overflow, so the client saw a socket close indistinguishable from a crash. Surface the cap that tripped and the backlog held when it did. The queue dropped its backlog before invoking onOverflow, so the counters were already zero at the callback. Snapshot them first and pass them through. --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: Neil <neil@stably.ai> |
||
|
|
4d82149fe5 |
fix(runtime): reject stale inventory after PTY lifecycle changes (#21014)
* fix(runtime): reject provider inventory across PTY lifecycle changes * fix(runtime): canonicalize SSH inventory generation keys --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: Neil <neil@stably.ai> |
||
|
|
b766f512ec | fix(editor): extract diff first-change auto-scroll to a hook to unblock main (#21738) | ||
|
|
b8f67a6266 |
Close workspace board when selecting sidebar worktree (#21737)
Co-authored-by: m4air <m4air@Mac.localdomain> |
||
|
|
85a3ba6d42 |
fix(terminal): align CJK IME preedit spacing (#19367)
* fix(terminal): align IME preedit to terminal cell grid * fix(terminal): preserve native shaping and reuse IME preedit on repaint * fix(terminal): preserve native shaping with bounded IME spacing runs * test(terminal): account for inline preedit subpixel rounding * test(terminal): keep the IME grid fixture wide at every DPI * chore: regenerate xterm patch after rebase * test(terminal): remove IME assertion lint findings * test(terminal): avoid reflective IME fixture access * test(e2e): run IME renderer matrix with WebGL available * fix(ci): restore editor line budget |
||
|
|
e4c7632db2 |
perf(terminal): skip kitty scans for plain PTY output (#21643)
* perf(terminal): skip kitty scans for plain output * fix(terminal): keep the kitty scan fast path total for absent chunks The new escape-byte fast path dereferences the chunk before the string concatenation that used to coerce a nullish value, so an unchecked caller now throws instead of no-opping. Normalize once at the top. Also type the AgentTerminalPreview connect mock against the real preload signature, which turns the stale bare-string replay fixture that tripped this into a compile error. |
||
|
|
abd310e5a3 |
perf(terminal): skip background SGR scans without ESC (#21646)
* perf(terminal): skip background SGR scan without escapes * perf(terminal): avoid duplicate renderer risk scans * test(terminal): pin the carried renderer risk scan tail The foreground renderer-risk scan splices the carried tail onto the incoming chunk before classifying it, and nothing covered that ordering: a pre-gate moved back above the concatenation would silently drop the refresh for a background SGR split across ConPTY chunks. Also pins the escape-free ASCII path and the shared global SGR pattern's statelessness across calls, since the background hit returns mid-loop. |
||
|
|
3e7da29767 |
feat(editor): add opt-in collapsed unchanged regions for file diffs (#11955)
* feat(editor): add opt-in collapsed unchanged regions for file diffs The combined "View All Changes" diff already collapses unchanged lines into expandable bands (DiffSectionBody sets Monaco's hideUnchangedRegions), but a single-file diff opened from Source Control renders the whole file. Reviewing one changed line in a long file means scrolling past everything else. Adds a General > Editor setting, default off, that applies the same Monaco option to the single-file diff viewer. Off keeps today's full-file rendering. The option is always emitted rather than omitted when off: Monaco retains the last applied value across an options update, so dropping the key would strand an open diff in collapsed mode after the setting is turned back off. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(settings): register collapse unchanged search entry * fix(editor): keep diff viewer under line limit * fix(editor): satisfy diff viewer line budget --------- Co-authored-by: Dan Cieslak <dcieslak19973@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: Neil <neil@stably.ai> |
||
|
|
844e9df98f |
fix(editor): keep long file paths distinguishable (#21631)
* fix(editor): keep filename visible in long paths * fix(review): keep diff filenames visible * fix(review): prevent path metadata overlap |
||
|
|
cef4416115 |
fix(mobile): name the narrow host header's controls and gate the drawer's hardware back on web (OTA phase C, C2.10) (#21729)
* fix(mobile): name the narrow host header's controls The header renders two toolbars and the phone sees the narrow one, whose controls carried neither a role nor a name. A screen reader could not find them, and C2.9's render check could only assert their absence at 390 px. The wide toolbar already names every control from the same state, so the fix is to say the same thing rather than invent wording: filter, sort, group, accounts, tasks and the search toggle take their wide sibling's role and label expression verbatim. The census names a seventh site the plan did not: the Reconnect button in the status bar above both toolbars, which is shared rather than narrow and has no wide sibling. It announces through its Text child today, so it takes the role and the string it already renders. No layout, style, handler or order changed; the diff is accessibility props only. The census parses the file with the TypeScript API and rules that every Pressable carrying an onPress has a button role and a name, and that a control both toolbars render is named the same way in both. It keys the pairing on the handler, because that is what makes two elements the same control, and asserts each shared handler is found exactly twice, so a control deleted from one toolbar cannot leave the naming rule comparing a group of one with itself. Red first, naming all seven sites by path and line. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): stop the right drawer arming hardware back on web React Native Web logs "BackHandler is not supported on web and should not be used." and returns an inert subscription, so inside the shell's page every open of this drawer put that line on the console and armed nothing. The gate is the one mounted-bottom-drawer and the file preview already carry, with the same comment stating the degradation: there is no hardware back in a WebView, and the shell owns the one the phone has. The drawer had no render test. This one mocks react-native, the safe area, gesture handler and Reanimated the way the bottom drawer's hand-back test does, and reads the call rather than the console: on iOS and on Android the handler is registered once for 'hardwareBackPress' and released when the drawer hides, and on web it is never reached. The three native cases are the control that keeps the web case honest; they passed before the fix, which is what makes the single red meaningful. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): type the right drawer test's element helper The tests-typecheck ratchet was red on the previous commit: the drawer's props declare `children` as required, so passing it as createElement's third argument left no overload matching. It is a prop here, and the helper answers a ReactElement rather than a return type borrowed from createElement. Test files sit outside `tsc --noEmit`, so only the ratchet sees this; it is the gate that exists because a type-level pin in an unchecked test proves nothing. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): write the right drawer test in JSX Lint was red on the previous commit and I ran it in the same command as the commit, so it landed: passing `children` as a prop to satisfy the type checker is exactly what react(no-children-prop) refuses. The canonical form settles both, so the test is JSX in a .tsx file and the drawer takes its body as a child again. The StyleSheet mock's generic needs the trailing comma a .tsx file requires. Re-proved in this form: with the web gate removed the web case fails and the three native cases still pass. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): derive the header's naming groups, and read a spread as unknown Round 1, four folds. The drawer's comment claimed the console line was observed inside the shell's page. It was not: the drawer's one caller is the review screen, whose route C4 serves, so no page closure reaches it today. The gate is pre-emptive and now says so in its own words rather than borrowing the bottom drawer's sentence. The naming rule iterated a hand-written list, so it only ever compared the six controls both toolbars render. Giving the two `actions.openFloatingWorkspace` sites different labels left the census green. The groups are derived from the discovered controls now, keyed by the handler text, so any handler this header presses from more than one place is compared and the failure prints both names. The declared list stays as the precondition it always was: each of the six is found exactly twice, which is what keeps the derived rule from holding vacuously over a file with no repeated handler. The scan read `Pressable` only and dropped any control whose `onPress` read as empty text, which is what a spread reads as. It reads `TouchableOpacity` too now, and a spread answers unknown rather than absent: a control whose handler or whose accessibility props arrive through one is kept, fails both rules, and prints `spread` rather than `none`, so it can never be mistaken for a control the scan judged. Red first on all four: the reviewer's disagreeing-label mutation, a spread over the a11y props, a spread over the handler, an unlabelled TouchableOpacity, and a shared control deleted from one toolbar. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): read a braced-empty label as unnamed, from one shared reader Round 2, two folds. Only a bare `""` or an omitted attribute read as unnamed, so `accessibilityLabel={undefined}`, `{''}` and an empty template all left a control with nothing to announce and the census green. Reproduced on both Tasks sites with each of the three shapes before the fix. The reader unwraps a braced expression now: a string or a no-substitution template answers its own text, and the identifier `undefined` answers empty, so all three read as unnamed. That reader was a near-verbatim copy in both censuses, which is how one of them could have gained this rule and the other kept the hole. It lives in one module under mobile-web-shell now, named for what it reads and typechecked by mobile tsc rather than by the ratchet alone. Both censuses import it and neither changed an assertion; their diffs are the deleted copies and the import. Red first, five mutations: the three empty shapes on both Tasks sites here, and `{undefined}` and `{''}` on the tasks Back, which the page census now catches too and did not before. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
93d245e358 |
Sort dev builds by timestamp instead of semver (#21720)
Dev build base versions can move backwards when a branch is cut before the latest main build. Their embedded timestamp is the authoritative "newest" signal for the picker. For dedicated release repos, compare publishedAt timestamps before falling back to semver comparison. |
||
|
|
9309350864 |
fix(chat): enforce legacy import byte budget during reading (#20976)
Co-authored-by: m4air <m4air@Mac.localdomain> |
||
|
|
89acf1e1fa |
fix(plugins): release diagnostic logs after successful uninstall (#21185)
* fix(plugins): retire log owners after successful uninstall * fix: address memory PR review regressions and withdraw false positives * fix(plugins): fence stale activation after uninstall * chore: allow durable plugin uninstall audit evidence --------- Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: Neil <neil@stably.ai> |
||
|
|
2741bdad38 |
fix(session): remember closed and initialized runtime tabs (#21630)
* fix(session): persist defaultTerminalTabsAppliedByWorktreeId Host persist snapshots wrote tabs but omitted this write-once map, and full session replaces / hydration treated omission as "never applied". Union the marker across persist and hydrate so default terminals are not re-spawned on every launch or re-attach. Fixes #18117 * fix(runtime): stop re-seeding a runtime-owned workspace the user emptied Focusing a workspace owned by a remote runtime created a terminal every time, and sometimes two. The mirror could never record the closed-last-terminal state. A host snapshot with no terminals produced `nextTerminalTabs === null`, which `withWorktreeEntry` turns into a deleted key -- and a missing row is exactly how every seeder spells "never initialized" (initial-terminal.ts). Keep an explicit empty row instead, so the remote path reads the same tombstone the local one already honours. A worktree that never had a terminal still gets no row, because `sameTerminalTabs` treats a missing row and an empty one as equal; removal frames and synthesized unpublished frames keep deleting, since neither is evidence the user emptied anything. The duplicate had a second cause. `requestedInitialTerminal` was a `let` inside the session-tabs subscription closure, so "one focus creates at most one terminal" held only for as long as that closure lived. Its effect re-runs whenever the environment, connection generation, pairing revision, or session-ready flag settles -- all of which move during a workspace switch -- so a second closure re-armed the flag while the first create was still in flight. That is the asymmetry in the report: one terminal when arriving from the landing screen, two when arriving from another workspace. Latch the bootstrap per worktree in a module-scoped set instead, modelled on web-runtime-wake-terminal-respawn.ts, released when the create settles. The closure flag stays alongside it so a failed create still does not retry on every later frame of the same subscription. Fixes STA-6173. * fix(runtime): harden the runtime-owned initial-terminal bootstrap latch Follow-up on the STA-6173 fix, addressing restore-time safety gaps found in review. - Decline the bootstrap on a synthesized unpublished frame (`UNPUBLISHED_WORKTREE_PUBLICATION_EPOCH` at version 0). That frame is the runtime saying "ask me later", not a host with zero terminals; seeding on it can duplicate a pane the host is about to republish after a restart. This is the same "ask me later" frame the tombstone write already refuses to treat as the user emptying the workspace. - Release the module-scoped bootstrap latch on worktree tracking teardown and environment teardown, mirroring web-runtime-wake-terminal-respawn. A create RPC that never settles during a disconnect would otherwise leave the per-worktree key set and suppress the next bootstrap after reconnect. Tests: - New per-worktree and per-environment latch-release cases. - New "unpublished frame declines" bootstrap case. - New hook-level regression pinning the second defect end to end: a forced active-subscription re-run while the first create is in flight seeds exactly one terminal (two on the pre-fix tree). * fix(runtime): key the initial-terminal bootstrap latch per environment Addresses review on the STA-6173 hardening. - Key the bootstrap latch by (environment, worktree), not worktree alone. A worktree id is `repoId::path` with no host component, so the same id can be live on two paired runtimes at once. The latch was cleared wholesale on any environment teardown, so tearing down environment A released environment B's in-flight key and a fresh B subscription could seed a duplicate — the STA-6173 defect through another door. Environment teardown now clears only its own keys; worktree teardown clears only that (environment, worktree). - Hold the latch after a successful create until a mirrored `tabsByWorktree` row exists. The snapshot refresh the create awaits can resolve on an empty, unconfirmed frame that leaves no row; releasing then let a later effect re-run seed a second terminal. A failed create still releases for retry. The latch claim/create/release now lives in web-runtime-initial-terminal-bootstrap-dispatch.ts, keeping active-session-subscription within its line budget. Tests (each mutation-tested against its own regression): - Cross-environment: a create for env B in flight, env A torn down, a fresh B closure must still decline. Fails when env teardown sweeps all environments. - Hold-until-row: a create that resolved without mirroring a row must not let an effect re-run seed again. Fails on unconditional release. * fix(runtime): release the bootstrap latch when the create returns failed createWebRuntimeSessionTerminal never throws: the operation catches RPC and network failures and returns `{ status: 'failed' }`. The dispatch helper released the latch only from `catch` (dead for that path) or once a tabsByWorktree row existed (false after a failure), so a failed create left the latch held and suppressed every later auto-seed for that environment's worktree until teardown -- the opposite of its own doc comment. Capture the outcome and release on a returned failure as well, so the next focus can retry. Regression: a create that resolves `{ status: 'failed' }` followed by an effect re-run must create again. Fails on the previous release condition. * fix(runtime): release a parked bootstrap on the mirror's next frame The previous commit released the latch on a returned failure, but a create that *succeeded* with no mirrored row yet had no release at all: the row-conditional check was the only exit for the success path, so a host that accepted the tab while the mirror never got a frame held the latch until environment teardown and suppressed every later auto-seed for the worktree. Give the latch two phases. `creating` blocks other closures while the RPC is in flight. A success with no row is parked as `awaiting-mirror` instead of held, and the next frame the mirror accepts for that worktree releases it -- that frame is the mirror's answer either way (a row now exists and the predicate declines on its own, or the host genuinely has no terminal and a retry is right). A create still in flight keeps its claim: releasing it on a frame would reopen the re-armed-closure race the latch exists to close. Also correct the closure-flag comment: `requestedInitialTerminal` is set only after the dispatch resolves, so a thrown create never sets it and a later frame may retry. The flag records that this subscription already owned a create; it never described a failed one. Regressions, each mutation-tested against its own term: - success with no row, then the mirror's empty answer, then a fresh closure -> must create again (fails when success-with-no-row is held instead of parked, and when the subscription does not call the frame release) - a mirror frame releases an awaiting-mirror claim but never a pending create (fails when the release ignores the phase) * fix(runtime): let a failed create retry inside its own subscription The returned-failure release freed the module latch but the caller still latched its closure-local requestedInitialTerminal whenever the dispatch reported it owned the create, including a create that returned { status: 'failed' }. A thrown failure never set it and retried on the next frame; since every RPC and network failure is reported as a return, the live path was the one that suppressed the whole subscription. The dispatch now reports false for a failure whichever way it arrives. * fix(runtime): close the second re-seed door on an emptied workspace ensureWebRuntimeWorktreeTerminalAfterWake read tabsByWorktree through `?? []`, and the only guard that could have seen presence was &&-gated on the value that erased it, so it could never fire for a tombstone. It runs on every activateAndRevealWorktree, not only after a wake, and a tombstoned workspace routes exclusively here: the stream-frame path returns at localTerminalCount === 0 before reaching its own guard. So the workspace was re-seeded on every focus regardless of the mirror fix. Two states shared one line and now do not. With no rows the workspace is being seeded for the first time and the decision goes to shouldAutoCreateInitialTerminal with presence read by Object.hasOwn; with rows present the question is whether a woke workspace's chrome outlived its PTYs, which the tombstone says nothing about. Both arms are pinned, because reverting only the respawn arm broke no existing test. * fix(runtime): stop the worktree-id re-key inventing a tombstone canonicalizeTerminalSessionWorktreeId read the source row through `?? []` and always wrote the target one, so a workspace with NO tabsByWorktree row came out the other side with an explicit empty one. That is the closed-last-terminal tombstone, and this PR's new activation-path reader honours it, so the re-keyed workspace never gets its initial terminal. Guarded on the source row's presence, which is the guard the sibling keyed maps six lines below already use. Mutating the guard to test emptiness instead of presence breaks the assertion that a real tombstone still survives the re-key, which is the distinction that matters. * fix(runtime): reconcile session tab bootstrap integration * fix(runtime): preserve terminal wake launch options * test(session): annotate cross-project persistence fixture * test(session): remove obsolete typecheck suppression * test(session): keep persistence regression in renderer project * refactor(runtime): remove inert snapshot recovery wrapper * fix(runtime): keep removal-frame import merge-safe * fix(runtime): deduplicate merged removal-frame import --------- Co-authored-by: Wooseong Kim <innocarpe@gmail.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
6a0200f416 |
test(mobile): certify the files page closure, 28 families and 125 goldens (OTA phase C, C3.2) (#21724)
* test(mobile): certify the files page closure, 28 families and 125 goldens
C3.2. The closure is re-derived at this base from the entries esbuild compiles —
the two `.web.tsx` files — and matches the design exactly: 28 families, 125
goldens, C1's 22 a strict subset, 6 families and 22 goldens added. Explorer
3441 modules / 304 local / 10 under `src/files`, preview 3666 / 330 / 19, union
342 local. No `mobileWeb.*` family appears, which is the tell that the native
switch was not measured.
Two tables at the route seam, as C2.6 splits its own: the explorer's one family
and the preview's five are separate evidence for two routes with separate
grants, and `c3-page-closure.ts` only spreads them over C1's.
C1's 22 families are inherited verbatim, not re-derived. Measured here, C2's
rule disagrees with 13 of those 103 pins — all 7 in `tasks.smart-source-search`,
all 5 in `host-worktree-refresh`, and `worktree-catalog-snapshot`. C5's
docstring says 10; corrected in this commit, comment-only. Every family C3
shares with C2 and with C5 is asserted equal object for object, and the three
committed pin files disagree on nothing.
Class totals: identical 66, result-absent-settlement 47, params-undefined 7,
result-absent-stream-release 3, write-ordinal 2. Pinned beside the per-id walk
because a table built wrong in a self-consistent way passes the walk.
Red-first, both halves: changing one verdict fails the totals and the
inheritance check by name; dropping a golden fails drift, totals and the census
count together.
What 125 certified does not say: `host-worktree-refresh`, inherited from C1, has
no byte-identical golden at all, so its 5 hold a class and not bytes. All six
families C3 adds have at least one. No scenario replays a save twice, so
`files.writeTerminalArtifact` is certified for one round trip and not for
idempotency; and no golden here subscribes, because the domain opens no stream.
The `config/scripts` precondition now derives both files routes and compares
their union against the C1 + C3 tables, with a second case proving each route
reaches a strict part of it — without which the union would pass with one route
contributing nothing.
Also folds pullfrog's open nit: the render check's comment claimed
`toContain('readme.md')` proved the encoded round trip, which a truncated path
would also satisfy; the url assertion beside it is what proves it.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* test(mobile): share the pin-source reader and finish C5's 10-to-13 correction
Two pullfrog findings on #21724.
`c5-page-closure.test.ts` still said the rule disagrees on 10 of the 103; only
`c5-page-closure.ts` moved in the last commit, so C5's two files disagreed with
each other and with C2's and C3's. The sentence now states 13 with the same
breakdown the other three carry. No assertion depended on the number. While
there, the comment records why C5 compares against the imported object rather
than the committed text and is still sound: it inlines its families instead of
spreading C1's, so there is no spread for an edited entry to launder through.
`pinsFromSource` was byte-for-byte identical in C2's and C3's suites — checked,
not assumed — and is now one module both import. It sits beside
`page-closure.ts` rather than inside it, named for what it reads: the module it
would have joined holds pure table arithmetic and this one reaches the file
system. The C2 suite's cases and assertions are unchanged; its diff is the
deletion of the copy and one import.
The shared reader keeps its teeth: making the wrapped-entry capture unmatchable
reds the inheritance check in both composed suites, which is the defect the
comment describes — three `result-absent-stream-release` pins once went missing
that way with an empty mismatch list.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* docs(mobile): name which leg of the path round trip each assertion proves
Round-1 nits 3 and 4. A second commit rather than a fold into `dc1de6e728`:
that one is already committed, and amending is off the table.
The render check's comment claimed the url assertion was the proof of the round
trip. It is the outbound leg only — what the page encoded into its own history —
and a screen that mis-decoded the middle of the path satisfies it and the title
assertion both. The comment says that now, and points at where the inbound leg
is proved instead: `mobile-file-path-route-encoding.test.ts`, which reads each
hazard shape back out of the href, and `mobile-file-preview-route.test.ts`,
which drives the normalizer the screen reads its params through. Both files
checked to exist, and read, before being cited.
The 10-to-13 edit left a 127-character line in a file that wraps at 100;
reflowed. It was the only over-length line the C3.2 commits introduced — the
others in both files predate this branch, and `oxfmt` accepts them because it
does not reflow comments.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* docs(mobile): say what the inheritance case is blind to, and finish the reflow
Round-1 item 3, plus a correction to my own claim.
The "inherits C1's families whole" case reads C1's committed text, which makes
it independent of the spread but not of C1's file: edit a verdict there and both
sides of the comparison move together. The docstring now says that, says what it
does catch — a C3 half redeclaring an inherited family, which the spread would
otherwise take silently from the last table — and names where the C1-file edit
is caught instead.
Both measured here rather than taken from the review. Flipping
`settings-repo-metadata-icons` in C1's file leaves this case green and reds
seven others: the class totals and the exclusion counts in C2's suite and this
one, both cross-series agreements, and C5's own inheritance case, which compares
against an independent literal rather than a spread. Redeclaring
`settings.repo-metadata` in the preview half reds this case along with five
more.
I also said last round that the 127-character line was the only over-length line
these commits introduced. That was wrong: I checked the two files in that commit
rather than the branch. Six lines over the 100-column wrap came in across four
files, including two I had just written in `c3-page-closure.test.ts`. All six are
reflowed, and the check is now over every line the branch adds rather than over
the files I happened to touch last.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* docs(mobile): state both redeclaration shapes, or neither count
Round 2's low finding. The docstring said redeclaring `settings.repo-metadata`
in the preview half reds this case "along with five more". The reviewer is right
that the number is shape-dependent, and right about both figures.
Re-measured, no truncation this time: one golden under the family's name reds
seven, because it also shrinks the census to 114 and leaves that family with no
byte-identical golden; the family copied verbatim with a single verdict flipped
reds five, the census unmoved at 125. The docstring states both with the reason
they differ, and says plainly that the count is not the claim — the load-bearing
half is that the spread takes the last table's entry and this case is what sees
it, which holds in both shapes.
The six came from reading a `head -6` of the failure list as the whole of it.
That is the same mistake in miniature as the one this file's own comments warn
about: an empty-looking result that was only a truncated one.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
|
||
|
|
335d111265 |
fix(mobile): name the Back control in every page-served screen (OTA phase C, C2.8) (#21715)
* fix(mobile): name the Back control in every page-served screen The C2.7 device proof could only reach the tasks Back by tapping the native control's coordinates: inside the shell there is no native chrome behind it, so a bare Pressable is absent from the accessibility tree and a screen reader has nothing to announce. The agent-history panel's Back carries a label but no role; the tasks one carries neither. Both gain `accessibilityRole="button"`, and the tasks one the app's own wording, `accessibilityLabel="Back"` — the spelling its thirteen siblings use, including the agent-history panel this screen sits beside. The host screen's Back already carried both and did not move. The new census is a rule rather than a count: it finds every Back affordance under the screen tree each page route serves — a Pressable rendering a ChevronLeft that either carries the back-button style or calls back — and requires a button role and a Back-shaped label on each. Its coverage assertion reads MOBILE_WEB_PAGE_ROUTES, so a route registered without a screen entry fails here, the way the flag census fails for a switched route. Red first on both offenders by name. Two tasks parity hashes move, and only the two a JSX prop must move: `semantics` 3,272 -> 3,274 for the widened host signature and the two new runtime strings, and the render-token stream 35,195 -> 35,203 for the eight tokens the two attributes are. Hooks, statements, declarations and styles hold, which is the evidence that nothing executable changed. Recorded in the test's comment. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): judge a Back control by what it does, and name the files pair Round 1 found the census discriminating on the ChevronLeft glyph, which separates nothing: dismisses sit in the same header slot with the same back-button style, so the rule claimed a dismiss and told it to be called Back, while the explorer's Close escaped only by drawing an X. The predicate is now the behaviour. A Back control is a Pressable whose press handler reaches a back call — a `back()`, `goBack()` or `onBack()` call, a handler named for one, or a local handler whose declaration makes one — or whose label already says Back. A control matching neither is outside the rule whatever it renders. The reviewer's mutation, a dismiss with the back-button style wrapping a ChevronLeft behind `onDismiss`, is claimed by the old predicate and passed over by this one, which is the whole of the difference. A spread now reads as unknown rather than absent, so a Pressable that hides these props behind one is a Back control that fails both rules instead of a control the scan never sees. No Pressable in the five trees spreads today, so the branch is inert and load-bearing. C3.1 registered the files explorer and preview, so the coverage assertion went red on its merge, as designed. Both screens join PAGE_SERVED_SCREENS and both Back controls gain `accessibilityRole="button"`; they already carried `Back to session` and `Back to files`. The tasks Back gains the `hitSlop={8}` its four siblings carry, so its touch target is no longer the glyph alone. Nine mutations: the dismiss stays out; a dropped role on each of the five controls and a dropped label on the tasks one go red by name; a spread fails both rules; a dropped table entry fails coverage; and dropping the host screen's label — its only detector, since `actions.leaveHost` is opaque — empties that tree and fails the presence assertion rather than passing in silence. Two parity hashes move for the one added prop and no count does: `semantics` holds at 3,274 lines with one changed, the host signature gaining `hitSlop`, and the render-token stream gains that attribute's four tokens, 35,203 -> 35,207. Recorded in the test's comment. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): assert a Back per page route, not per screen directory Round 2 found the presence assertion reading directories while two routes share `src/files`, so the explorer's Back answered for the preview's as well: rewriting the preview's control to `onPress= {handleClose}` with `accessibilityLabel="Close"` left that route with no accessible Back and the suite green. Reproduced exactly, then made red by this change, which names the route and the module it failed in. Each PAGE_SERVED_SCREENS entry now names the module that renders its route's Back, and presence is asserted per entry. The rules still read whole trees, so a Back added beside a screen is ruled as well as the screen's own; the tree is derived from the module's directory rather than declared twice. The failure prints the offending list instead of comparing two values a reader cannot tell apart. The doc comment claimed the preview was carried by the label half of the predicate. It is not: `requestBack` is named for what it does, so the preview is found behaviourally like the explorer, the tasks screen and the agent-history panel. Dropping the label from each of those three leaves it detected and failing the naming rule, which is the evidence. Only the host screen depends on the label half, because `actions.leaveHost` is a member access this rule does not follow. The admitted residual gap stays admitted and the predicate is not widened for it: a second Back control in a screen that already has one, with an opaque handler and a label that does not say Back, is still outside what this rule sees. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
f016d38e9c | test(e2e): prove Pi EOF removes sidebar agent row (#21722) | ||
|
|
ac024d4f05 |
feat(mobile): serve the files explorer and preview from the page (OTA phase C, C3.1) (#21710)
* refactor(mobile): take the files screens' router from the handoff seam Inside the shell's page a screen is one document standing in for one screen, so a target the page does not render has to be handed back to the app that does. `useRouteHandoff` is where that decision lives, and its web sibling is the only thing that makes it; both files screens held expo-router's own `useRouter`, so on the web the explorer's Back and the preview's Back would post nothing and a target outside the page would paint Unmatched over the page it is on. Natively this is the same object — `route-handoff.ts` is `useRouter()` — so no behaviour moves here, and `back()` stays expo-router's until the navigate-back verb lands and the seam starts wrapping it. A census rather than a behaviour test: neither screen's own tests can see the difference, because a push that is never handed off still works for a target inside the page. It walks this directory, refuses a value import of expo-router, and names the two screens that must hold a router so a walk that found nothing fails instead of passing empty. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): let the shell stand in for the two files routes Both route files take the index.tsx shape — flag, MobileWebShellScreen, native screen as fallback — and both gain the `.web.tsx` sibling that shape forces. Inert until the manifest lists these routes: the shell answers `native-route` for a route the bundle does not name, which is what `fallback` renders, and the flag is `__DEV__`-only besides. Listing them waits on C2.3 and C2.5. The sibling is not a precaution. The manifest defers every route behind `import()`, so a native-only route module is invisible until the page opens that route; the render check now opens both and, without the siblings, painted `expo-modules-core.requireNativeViewManager is not available on web` instead of the screen. That is also why the two cases render the route rather than asserting a file exists. The file path never becomes a path segment: only `hostId` and `worktreeId` are spelled into the pathname, encoded, and everything else — `relativePath`, `absolutePath`, `cwd`, `pathText` — is a param, which is how a `/`, a space or a `..` stays out of the segment vocabulary the bridge holds a route to. The preview render case proves the round trip on `docs/my notes/readme.md`. `mobileFilePreviewShellParams` drops a param the normalizer left `undefined` rather than sending it empty, because the page reads these back through useLocalSearchParams where `line: ''` and no `line` are different screens. Its test drives the normalizer rather than a hand-written literal: the literal omits the key entirely, so it held with the filter removed. The preview case also records what React Native Web says out loud — BackHandler is inert on web, so Android back inside the page skips the unsaved-draft prompt. Named in the assertion rather than filtered out, so closing it is a change to that line. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): ask about an unsaved draft in the screen, not through Alert React Native Web's `Alert` is `static alert() {}`. Inside the shell's page that made Back with an unsaved terminal-artifact draft a button that did nothing at all: no prompt, because the dialog is a no-op, and no navigation either, because the code took the branch that shows one. Silently, with nothing on the console. The prompt is now a row under the header. Not `ConfirmModal`, which every other confirm here uses: that is a `BottomDrawer`, and C1.9 has Reanimated's animated styles never reaching the DOM node on WKWebView, so on iOS in the page the drawer parks off-screen and Back would be dead a second way. This paints the same on every platform with no animation behind it. Hardware back is registered natively only. React Native Web's `BackHandler.addEventListener` logs "BackHandler is not supported on web and should not be used." and hands back an inert subscription, so the guard never armed there regardless; the render check asserted that console error on main and now asserts none. The degradation is real and stated rather than hidden: Android back inside the page pops the native stack without asking, and the page's own Back control is where the question lives. The decision moved to a hook so it is testable without a screen: the prompt also drops itself when the draft it was about is saved or reverted, which is a state `Alert` had no way to be in. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): keep expo-haptics' DOM shim out of the page expo-haptics has a web build, and with no `navigator.vibrate` — iOS Safari, which is the WebView the page runs in — it fakes a haptic by appending a hidden `<label><input type="checkbox" switch>` to `document.head`, clicking it, and removing it, once per call. C1.9 traced a long press that never fired on the worktree list to exactly that stray click, and the file explorer calls `triggerSelection` on every row tap, so C3 is the first domain to fire it per tap rather than per long press. `haptics.web.ts` answers the same five names with nothing. A phone holding the page is a phone whose native app is right there with the real haptics, and a missing tap feedback is worth less than a tap that does not register. The test reads the shipped bytes rather than the import, because that is the claim: with the override removed the bundle carries `ariaHidden` and `pointer: coarse`; with it, neither, nor the `setAttribute("switch"` that does the clicking. Not `navigator.vibrate` — react-native-web's own Vibration export calls that and touches no DOM until something invokes it, which cost this test one wrong red before it was narrowed. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): keep the files routes native when the page could not be given one A file path is a param, so `/`, spaces and `..` all cross safely — but `BRIDGE_MAX_ROUTE_PARAM_CHARS` is 1024 and a Windows long path is not bounded by anything the user cannot exceed. The symptom is not the blank document the design predicted, and the correction matters: `bridge-host.ts` already parses the route against the page's own schema and drops it to `null` when it fails, so `init` arrives naming no screen and the page paints "Update Orca to open this workspace" — a wrong message about a fine app, over a native screen that works. Deciding before the switch instead leaves the route native, which is where every route starts. The schema is the predicate rather than a copy of its bounds, so the rule cannot drift from the half that matters, which is the half the page reads. The same call also refuses a `worktreeId` the segment rule will not route: `..` survives `encodeURIComponent`, which is the C1.8 class. The tests assert the schema really refuses each input before asserting the guard does, so neither case can pass by being impossible. This belongs in the shell beside the schema; it is in the files domain while the contract files are the C2 lane's. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): pin what keeps a file path out of the route vocabulary Seven shapes, one case each rather than a representative: a plain path, a space, a dot segment, an already-encoded slash, a fragment, non-ASCII, and an absolute path. Each is checked in the two directions a path travels — the href the shell writes into the page's history, and the href the page would hand back — for both the pattern accepting it and the path coming back out of the query unchanged. The counterfactual is in the file: the same paths spelled as a segment are refused. Without that, the cases above would hold for a rule that was never doing any work. Mutating `stringifyRouteHref` to join its query by hand instead of through `URLSearchParams` fails three of them. Also fixes two new test files the tests-typecheck ratchet caught: the partial `react-native` mock needs a typed `addEventListener`, `act` will not take a callback that returns a value, and `findAllByType('Pressable')` does not typecheck against `ElementType` — the neighbouring files that do it are grandfathered, so the tag comparison goes through a helper instead. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): derive the discard prompt instead of clearing it in an effect Both changed-code gate findings, which the lane had not run until the last commit. React Doctor is right: the effect that cleared the prompt when the draft went away adjusted state after a prop changed, so a save landing while the prompt was up painted one frame still offering to discard nothing. The prompt is now `asking && hasUnsavedDraft`, which cannot be stale by construction, and the test that covers it passes unchanged. The hoisted mock's `as` on a string literal is gone too: the literal narrows on its own and the tests reassign it, so the holder is annotated instead. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): add the files routes to the hybrid shell flag census The census pins every file that reads `useMobileWebShellEnabled`, because a reader nobody listed is how a dark feature stops being dark. C3's two routes are deliberate entries: each has a native screen behind it as `fallback`, and each is inert until the manifest lists the route. Found by the full mobile suite rather than by the files subset this lane had been running per commit. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * feat(mobile): serve the files explorer and preview from the page The last C3 commit: both routes join MOBILE_WEB_PAGE_ROUTES, and the shell starts rendering the page for them on a phone with the dev flag on. Grants are not the same for the two, and the difference is the point. Both take `navigate` (Back pops the native stack, and the explorer's rows open the preview beside it) and `storage` (the shared components the host layout renders above them). Only the preview takes `externalLink`: a Markdown preview renders links and `MobileMarkdown` opens them through the platform seam. The explorer does not, and measuring is what says so rather than reading. Every page route reaches `external-link.web.ts` — `/h/[hostId]` and agent-history included, both granted nothing for it — because the protocol wall in the shared host layout imports it. So closure membership is not the oracle for a grant; the question is whether the route's own screens call it, and only the preview's do. `MobileMarkdown` is in the preview closure and absent from the explorer's, which the census now asserts in both directions. Neither route writes a clipboard, so neither takes `native.clipboard.write`; the census pins that as the absence of both `ExpoClipboard.web.js` and the clipboard seam, with the tasks closure as the control that the probe can see one when there is one. The seam predicate moved into a module both censuses import rather than being restated per series: two spellings of one rule drift, and this one is a regex. Red-first: both manifest assertions failed on the new entries before they were updated, and routing `MobileMarkdown` around the seam fails the preview's census while leaving the explorer's passing, which is the asymmetry the grants encode. Closure sizes as the page ships them, extensionless so the `.web.tsx` is what is measured: explorer 3439 modules / 302 local / 10 under src/files, preview 3667 / 331 / 20. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): read the files route's ids as one value and key the shell on them Two round-1 findings, both reproduced before the fix. A repeated query key reaches `useLocalSearchParams` as an array, and the explorer read `hostId` and `worktreeId` bare. `String(['a','b'])` is `a,b`, so the template built `/h/host-a%2Chost-b/files/wt-1%2Cwt-2` — a single segment the bridge's rule accepts, and the shell would open a page for a host nobody has. Read through `firstParam` now, as the tasks and agent-history switches do. The preview already went through `singleParam` and is unchanged. Neither switch keyed `MobileWebShellScreen`, where `index.tsx`, `tasks.tsx` and agent-history all do. A host captures the grants its session opened with, so a screen reused across a route change keeps authorising frames under the grants of the route the page has left; only a remount drops that bridge. Both are keyed on the route pathname now, with agent-history's reason. The new route test is the agent-history one's shape. It caught both: the array case landed on no route at all, because `name` was an array too and the schema refuses a non-string param value, and the two lifecycle cases saw a prop update where a remount was owed. It also needs agent-history's `lucide-react-native` mock, since `firstParam` lives in the source-control barrel. `name` is now omitted when empty rather than sent as `name=`, matching the two switches beside it: an absent label lets the panel derive its own. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): confirm a discarded draft with the app's own modal Round-1 findings 3, 4, 5 and the minor one. **ConfirmModal, not the bespoke row.** The row existed because C1.9 had Reanimated's animated styles never reaching the DOM node on WKWebView, which left every BottomDrawer parked off-screen. C1.10 (`b7c06900e2`, an ancestor of this branch) fixed that with a dependency array on the mapper hooks, and the drawer render check now holds it on WebKit as well as Chromium. With the reason gone the row does not stand on its other merits: `Alert.alert` was modal on native before the page existed, and the row quietly changed that for phones too, so the app's own confirm is both the idiom and the closer behaviour. `MobileFilePreviewDiscardPrompt`, its test and its thirty style keys are gone; the hook's state machine and its tests are unchanged. **The encoding test claimed more than it pinned.** Hand-joining the query reds only three of the seven shapes; `docs/readme.md`, `../etc/passwd`, `docs/日本語.md` and `/logs/run.txt` are encoding-neutral in the query, whose pattern half is `[^#\s]*` and admits a slash, a dot segment and non-ASCII verbatim. Rather than narrow the claim in a comment, the split is now pinned by behaviour: each neutral shape must survive the query unencoded, each load-bearing one must not. Moving `docs/readme.md` between the lists fails it. **The manifest comment named one shared-layout opener and there are two.** The New Workspace source field, which the sidebar renders on a wide layout, opens a URL through the seam as well. Both are the shared layout's and every `/h` route reaches both, `/h/[hostId]` included with no `externalLink`, so the tablet tap is dead on all of them — recorded here as pre-existing rather than fixed, since the grants do not move. **Minor:** the dot-segment case in the guard test now asserts the schema refuses the route before asserting the guard returns null, as the length case does. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): stop every page drawer logging a BackHandler error when it opens Round-2 findings. **The registration belongs to the drawer, and that is where the guard went.** `mounted-bottom-drawer.tsx` armed `hardwareBackPress` whenever a drawer was visible and interactive, with no platform check, so the hook's claim to have dropped that console line held only while its prompt was closed — and every page drawer since C1 has logged it on open. Platform-gated at the drawer now; the hook's comment says so rather than claiming the credit. **Nothing had ever opened a modal in a browser.** The render check next door mounts both files routes and reads what they paint but taps nothing, so `ConfirmModal` inside the page — a BottomDrawer, so Reanimated, a portal and a gesture handler — was unproved. A new render file loads an editable terminal artifact through the harness's scripted reply, edits it, taps the page's Back, and asserts the prompt's title is up and no BackHandler line is on the console. Red first on exactly that line; the prompt itself painted, which is also the first proof on a browser that C1.10's fix carries a real drawer in the page. A second case answers Stay and checks the draft survives. Its own file rather than the render check's, which is at 482 of the 600-line cap; registered in pr.yml. **The encoding rule was stated wrong.** Two rules decide it and neither is about paths: the pattern's query half refuses whitespace and `#`, and `URLSearchParams` is form-urlencoded, so it reinterprets `&`, `+` and a valid `%XX`. `a+b.ts` reads back `a b.ts` and `a&b.ts` reads back `a`, so both are load-bearing; `a=b.ts` and `a%b.ts` are not, because only the first `=` splits the pair and a lone `%` begins no escape. A newline joins the load-bearing list as the refused shape rather than the altered one. **The web sibling read its params bare** where the native one uses `firstParam`. Not reachable — the page only arrives through `init.route`, whose params are already `Record<string, string>` — but the two files are meant to be one screen. The preview keys on the pathname alone, and the comment now says why that is enough: every caller in this tree pushes. Closures after this: explorer 3441 / 304 / 10, preview 3666 / 330 / 19. The explorer grew two modules because its web sibling now reaches `firstParam`. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): give the explorer the grants the preview needs, and key on the route Bot findings, one of them a real gap. **Pullfrog is right, and my grant oracle was half a rule.** Grants resolve once, from the route the shell opened: `grantsForRoute` reads `session.routePathname` and `init.grants.native` carries the answer for that session. The explorer's rows push to the preview, and because the preview is a page route that push stays inside the same document — no second `init`. So a preview opened that way runs under the explorer's grants, and a Markdown link in it was refused by `notifyExternalLink` with nothing on screen to say why. "Does the route's own screen call it" was right for a route's own screens and wrong for the routes it reaches in-page, so the explorer now declares `externalLink` as a transitive grant, with the comment saying that rather than claiming it opens links. The census pins the pair as a superset; removing the grant reds it. **The seam regexes matched one quote style.** A double-quoted `react-native` specifier walked past both censuses unseen. Both styles now, with the predicate tested directly for the first time. **The discard request outlived its draft.** `asking` stayed set after a save or a revert, so the next edit re-showed the prompt with no Back request behind it. The request is now dropped when the draft it was about goes, adjusted during render rather than in an effect — the shape React Doctor named in the round-1 fold. Red first: save with the prompt up, edit again, prompt is back. **CodeRabbit's keying comment is a correctness point, not the question I answered.** The page learns its route exactly once, out of `init`, so a same-path param change — another file in the same worktree — left the shell mounted and the page still showing the file it was opened on. My comment claimed "the screen reloads the preview from the param either way", which is true only with the shell absent. Both switches key on the whole route now, params included; two tests cover the same-path case and both red on a pathname-only key. `build-mobile-web-app-bundle.test.mjs` hit 601 of its 600-line cap on the way, so the two manifest assertions now share one expected list instead of repeating it. Closures unchanged: explorer 3441 / 304 / 10, preview 3666 / 330 / 19. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * fix(mobile): make the seam test import the module it is testing Round 3. **The blocker is mine and the reviewer's diagnosis is exact.** The seam predicate test imported an absolute path into this lane's worktree. On CI that module does not exist and it takes the whole `config/scripts` suite down; here it resolved to the same file by accident, so the test was green against a tree rather than against the checkout — which is why reverting the double-quote fix left it passing and the predicate untested. Relative now, and proved: reverting the fix in place reds both double-quoted cases, which is the first time this test has failed for the right reason. Every file this PR touches is grepped for `/Users/` and `orca-lanes`; none carries a path. **Three comments outlived the grant change.** The two lists became equal when the explorer took `externalLink`, so "longer than the explorer's" and "declared with different grants" were both false. Corrected to what is actually true: the lists are equal and the reasons are not — the preview has its own consumer in `MobileMarkdown`, the explorer has none and declares the grant because its rows push to the preview in-page. **The duplicated serializer is pinned rather than imported.** `shellRouteHref` lives in `page-bootstrap.ts` beside the page's RPC client and its document channel, so a native route file importing it would pull both into the app. The copy stays, and a test asserts the two agree on three routes; dropping the empty-search branch reds it. **Recorded, not fixed:** the sidebar `HostScreen` pushes to `/h/<id>/tasks` through the handoff, which is local, so on a tablet the tasks page runs without `native.clipboard.write` from any page route and its copy actions refuse silently. Pre-existing since C2.1 for the worktree list and agent history. Named in the explorer's manifest comment as the known remaining hop, with the fix being a handoff rule in its own PR. The equality pin needed `it.each<BridgeInitRoute>`: the inferred table is a union whose members carry `?: undefined`, which the ratchet caught. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
76063e7ab1 |
test(mobile): certify the tasks page closure, 70 families and 266 goldens (OTA phase C, C2.6) (#21712)
* test(mobile): read a page closure's run totals through one reader The C5 gate counted the run's classes inline. C2 needs the same count over its own closure, and two spellings of "what the run tallied" can disagree while both stay green, so the loop moves next to `pageClosureTotals` where the table-side count already lives. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): certify the tasks page closure, 70 families and 266 goldens C2 moves the tasks screen to the web, so the goldens recorded at a call site inside `app/h/[hostId]/tasks.web.tsx` and `app/h/_layout.tsx` are the ones whose divergence would be this domain's. Each is pinned by id: the suite's own counts run over 787, where one of the other 521 can pay for a closure golden that stopped replaying. C1's 22 families are inherited verbatim rather than re-derived — C2's rule disagrees with them on 10 of the 103 — and the rule decides only the 48 this domain adds. The pin is split at the domain's seam, one work item opened versus choosing which to open, because the table is 409 lines of data and `max-lines` is not a thing to disable. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test(mobile): correct the C2 pin's inheritance count and census scope Two comments overstated what was measured. The rule disagrees with 13 of C1's 103 inherited pins, not 10 — the 10 was copied from C5's file, which carries the same error over the same 22 families — and the breakdown is now named so the number can be re-derived rather than trusted. The census reads the committed table and does not re-derive the closure, so a golden arriving in a pinned family is caught while a new family entering the closure is not. That was true and unsaid, which is the worse of the two. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * test: derive the tasks page closure's family set instead of trusting the pins Round 2 folds, three. The pin tables walk the families they already hold, so a scenario recorded at a call site the route already imports lands in a family nobody pinned and every assertion stays green. `mobileWebAppRouteClosure` runs in a quarter second and `config/scripts` already imports it, so the derivation is now a test: the family set the closure reaches must equal the union of the three committed tables. C2's inheritance check read the object its own table spreads, which cannot disagree with itself; it now reads C1's file as text. What that does and does not hold is written down, because a verdict edited inside `c1-page-closure.ts` is green there either way — C2 inherits whatever C1 commits. The gate's C1 block gains the run-totals assertion C5 and C2 already had, which is the check that edit does fail. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb * ci: run the page-closure family check in the job that installs mobile deps Its closure half asks `mobileWebAppDependenciesPresent()` first, so outside the `mobile_web_app` job it skips itself and the precondition it exists to be never runs. That job sets `ORCA_MOBILE_WEB_APP_DEPS_REQUIRED`, which turns the same question into a failure. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
3cd674fdff |
Make jump-palette selection state higher contrast (#21718)
- Increase light-mode contrast: foreground mix 12%→13%, ring 18%→19% - Extend selectors for .jump-palette-command scope and add class |
||
|
|
b6b974cb82 |
fix(terminal): clear stale agent identity after shell exit (#21714)
* fix(terminal): clear stale agent identity after shell exit * test(identity): update resolver decision table |
||
|
|
c4c9486470 |
fix(pi): do not install extensions for disabled agents (#21711)
* fix(pi): honor disabled agent extensions in PTY setup * fix(pi): preserve fresh OMP config when disabled |
||
|
|
97763edac0 | fix(pi): ignore idle utility dialogs in completion status (#21707) | ||
|
|
b6e8b1a7b2 |
feat(mobile): serve the tasks screen from the page, with its seams (OTA phase C, C2.1 + C2.5) (#21694)
* fix(mobile): encode the host id in the tasks workspace-creation href (OTA phase C, C2.1)
`use-mobile-tasks-workspace-create-actions.tsx` built
`/h/${hostId}/session/...` with the host id interpolated raw — the C1.2 class.
A host id carrying `/`, `#`, `?` or whitespace reaches the wire as an href
`BRIDGE_ROUTE_HREF_PATTERN` refuses, the handoff falls through to the local
router, and expo-router's Unmatched paints over the page.
Deleted rather than patched: `hostNewWorktreeSessionRoute` already builds
this exact href with both segments encoded, and already has the test that
pins it. The screen now calls it.
The census that caught it stays: no module under `src/tasks` may interpolate
into `/h/${...}` without encoding, which is the rule rather than this one
line. Three refactor-parity hashes move with the statement change and are
recorded in that file the way every earlier movement is.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): route the tasks tree's external links through the seam (OTA phase C, C2.1)
Ten of the twelve call sites in the tasks page closure: the nine under
`src/tasks`, swapped by one export in the dependency barrel, and
`MobileMarkdown.tsx`, which imports react-native directly and is edited in
place.
Inside the shell's WebView react-native-web's `openURL` calls
`window.open(url, '_blank')`, which both shells refuse — iOS returns nil from
`createWebViewWith`, Android false from `onCreateWindow` — and resolves
regardless. Every one of these sites would have reported success into a tap
that opened nothing.
The barrel's `Linking` is typed `{ openURL: (url: string) => void }`, so a
`.catch` on it is a compile error rather than a handler for a rejection that
cannot arrive; the seam names its own failures. `MobileMarkdown`'s own
`.catch(() => {})` goes with the swap for the same reason.
No parity hash moved: the barrel and `MobileMarkdown` are outside the
refactor-parity family's source set.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): route the shared screens' external links through the seam, with a census (OTA phase C, C2.1)
The last two of the twelve call sites in the tasks page closure:
`ProtocolBlockScreen.tsx` and the `openExternalUrl` prop wiring at
`host-screen-overlays.tsx`.
Both are shared with native routes and with the already-live `/h/[hostId]`
page, so this changes that page too: its external links go from the measured
`window.open` no-op — which both shells refuse and which resolves anyway — to
a URL handed to the shell. Nothing changes on a phone, where the seam is
`Linking.openURL` unchanged.
The `openExternalUrl` prop chain is retyped `(url: string) => void` with it,
and `SmartWorkspaceSourceField`'s `.catch(() => {})` goes: the seam names its
own failures and never rejects, so that was a handler for a rejection that
cannot arrive.
The census is the rule rather than today's twelve sites: no module in the
tasks page closure may reach react-native's `Linking`, by name or through a
namespace import. It reads the closure from a new builder export —
`metafile.inputs` for `_layout` plus the route, which is one definition of
what a page contains — and checks which module the name comes from, not which
text a call site writes, since the tasks tree still calls `Linking.openURL`
and that `Linking` is now the barrel's seam-backed export. Confirmed to
discriminate: restoring one react-native import turns it red.
A second case pins that the seam is in the closure, so an empty offender list
cannot also mean a page that reaches no link code at all.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): write the tasks clipboard through the shell's verb (OTA phase C, C2.1)
The two `Clipboard.setStringAsync` sites in the tasks page closure move onto
a seam, `src/platform/clipboard.ts` with a `.web.ts` sibling, registered in
the overrides.
A hook rather than a function because the web form needs the page's bridge
client, which is React context. Native is `expo-clipboard` unchanged. Web
calls `native.clipboard.write` through `useNativeVerbs`, because
`expo-clipboard` on the web is `navigator.clipboard` and needs a secure
context: the iOS shell serves the page from a custom scheme and Android from
`https`, so that path would work on one platform and silently not on the
other, with nothing at the call site able to tell.
Both seams reject rather than return false, and both call sites already wrap
the write in a `catch` that puts the message on screen — so a write that did
not land says so instead of showing "Copied". A route that has not declared
`native.clipboard.write` is refused before a frame is sent and lands in that
same `catch`; the route declares it in the entry commit.
Two parity hashes move, the hook list and the statement hash, each by one
entry, and are recorded in that file. `semantics` holds, as do render and
style: no RPC call, method literal or JSX host signature changed.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): hand the tasks Back button to the shell (OTA phase C, C2.1)
The tasks header's `router.back()` reached expo-router through the dependency
barrel, and inside the page that moves nothing: the document holds the single
history entry the entry wrote with `replaceState`. The stack with somewhere
to go is the native one the shell pushed the page onto.
One line in the barrel, as with `Linking`: `useRouteHandoff` is router-shaped,
so every call site is unchanged. On a phone it is expo-router. Inside the page
it keeps a route the page renders and posts `navigate-back` for a Back the
document cannot serve — the C2.2 seam, which until now had no consumer.
No parity hash moved: the barrel is outside the refactor-parity source set,
and no call site changed.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): render mermaid as its own source box on the web (OTA phase C, C2.5)
`MermaidDiagram` is in the tasks page closure, reached through
`MobileMarkdown`, and it renders the diagram inside a sandboxed `WebView`.
`react-native-webview` is a native component with no browser counterpart:
importing it runs a codegen lookup that throws, and the route manifest imports
every route, so one such import takes the whole page down rather than one
diagram.
The web sibling renders the labelled source box the native component already
falls back to on a parse or render error, with that component's own styles, so
the degradation looks like a state the product already has rather than a
second design.
Not a browser renderer, and the reason is not reach: mermaid is a browser
library and the engine bundle is vendored. It is that the native path's safety
comes from the WebView it runs in — `buildHtml` escapes `</script>` and the
U+2028/U+2029 separators because diagram source is untrusted agent and PR
content — and a DOM path has no such sandbox, so it needs its own escaping and
its own proof. That is a change of its own, not a smaller version of this one.
Registered in the overrides, whose gate fails on an unlisted `.web.*` file.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* feat(mobile): turn the tasks route on for the page (OTA phase C, C2.1)
The entry: `/h/[hostId]/tasks` joins `MOBILE_WEB_PAGE_ROUTES`, the route file
becomes the shell's flag switch in `index.tsx`'s shape, and a `.web.tsx`
sibling renders the screen directly, registered in the overrides.
The screen moves to `src/tasks/MobileTasksScreen.tsx` first, verbatim — body
byte-identical, imports rewritten to `./`. It has to: under the builder's
`resolveExtensions` a web sibling importing `./tasks` resolves back to
itself, which is why every other shell route's screen already lives in `src`.
The parity family follows the file rather than the path. `TASKS_ROUTE` leaves
`MOBILE_TASKS_SOURCE_FILES` — `SOURCE_PATTERN` already matches
`MobileTasks*.tsx`, so listing it too would double-count — and the execution
reader points at the new file. Measured rather than predicted: all six
refactor-parity cases pass unchanged. No hash moved, including the family
text and declaration list, because the new name sorts where the route path
sat.
The route declares `navigate`, `storage`, `externalLink` and
`native.clipboard.write`, which the grammar fold made expressible and
per-route scoping makes meaningful: it is granted those and not the rest of
what this shell implements.
The browser check covers what only a browser answers — every module in the
closure evaluating under React Native Web, `taskSource` surviving the
handshake into the page's own URL, and the route's chunk arriving on a
client-side navigation. It states plainly what it does not cover: the three
seams are reached from controls that need provider data the double does not
serve, so a case posting those frames directly would prove the transport and
read as a tap it never performed. Both new checks join the `mobile_web_app`
job.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(config): resolve a route closure the way the bundle ships it (OTA phase C, C2.1)
`mobileWebAppRouteClosure` took the route's explicit `.tsx` path as an entry
point, so esbuild used that file directly and `resolveExtensions` never ran.
For a route with a `.web.tsx` sibling that measured the native switch, which
no browser loads: the tasks closure came back carrying
`MobileWebShellScreen`, and with it a `Linking` import the census then
reported as an offender.
Extensionless now, so the closure is the one the page actually contains:
3775 modules, 428 local, with `external-link.web.ts` and `clipboard.web.ts`
in it and the shell screen out.
The route-manifest pins move with the tasks route joining
`MOBILE_WEB_PAGE_ROUTES`, in both the declaration check and the built
manifest.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): cover the clipboard seam, close two page escapes, share the mermaid props (OTA phase C, C2.1)
Four from round 1.
The clipboard seam shipped untested. Both halves have one now: the native
form rejects when `setStringAsync` answers false and resolves when it does
not, and the web form is driven through the real port pair — resolving on a
reply, rejecting when the shell says the pasteboard refused, and rejecting on
an ungranted route without putting a frame on the wire.
The tasks barrel still re-exported `expo-clipboard` with no consumer, which
kept `ExpoClipboard.web.js` — the `navigator.clipboard` path this series
exists to avoid — inside the page closure. Deleted, and asserted as the
module's absence from that closure rather than as a count of importers: a new
import puts the file back whoever writes it.
`ProtocolBlockScreen` reached expo-router's singleton for its way out to the
host list. A singleton is the one shape the handoff cannot intercept — it is
not a hook, so the page's bridge client is never consulted — and `/` is a
route the page does not carry, so inside the shell that replace rendered the
root route in the WebView instead of leaving it. Pre-existing and live via
`/h/[hostId]`; routed through the handoff now. Two suites' `expo-router`
mocks gain the hook the handoff reads.
`MermaidDiagram.web.tsx` redeclared its props; it imports the native
component's type, so drift fails tsc.
No parity hash moved: none of these files is in the refactor-parity source
set.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* style(config): use endsWith for the clipboard module check
The changed-code gate refuses a dollar-anchored regex where `String#endsWith`
says the same thing. No behaviour change.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
* fix(mobile): close the href census gap, read route params through firstParam (OTA phase C, C2.1)
Five from round 2, two of them real.
The raw-interpolation census inspected only the leading `${...}`, so
`` `/h/${encodeURIComponent(hostId)}/session/${worktreeId}` `` passed it — and
a worktree id carrying `/`, `#`, `?` or whitespace breaks the href exactly as
a host id does. It now refuses any hand-built `/h/...` template with any
interpolation left raw, whichever segment it is. Proved against exactly that
shape in a throwaway before the change, which the old rule admitted.
The tasks switch read `hostId` and `taskSource` as plain strings. expo-router
hands back an array for a repeated query key, so a duplicate `?hostId=` built
`/h/host-a%2Chost-b/tasks`; both go through `firstParam` now, as the
agent-history switch does. `index.tsx` is untouched, per the Phase D list.
Three in the render check's prose: the header claimed the browser proves the
three seams fire from a tap, which the file's own closing note denies; a
module count repeated a number the closure test already pins; and a `replies`
parameter was threaded through without ever being supplied.
Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
|
||
|
|
b0cbb919ba |
fix(pi): use Pi configured provider for Source Control AI defaults (#21693)
* fix(pi): let Source Control AI use Pi configured default When Orca runs Pi for automatic branch names or commit messages without an explicit model override, omit --model so Pi resolves its configured provider. Preserve explicit discovered model selection and add regression coverage. * fix(pi): preserve discovered fallback for non-Pi agents Keep the configured-default sentinel behavior limited to agents whose default is the explicit CLI sentinel. Other dynamic agents still fall back to the first discovered model when their static default is unavailable. * test(pi): pin configured-default dry-run arguments Prove Source Control AI does not render the Pi configured-default sentinel as a literal model argument, and assert explicit model flag pairing positionally. |
||
|
|
3ad6b7e46e |
chore(mobile): repin the recording corpus to main's tip after #21674 (#21690)
#21674 re-recorded the corpus with `baseline` set to its own branch commit `1006ad670a`, which the squash merge left unreachable from main. The pin guard on the push to main fails, and so does the pin job on every open pull request, since the merge ref cannot reach that commit either. Repin to main's tip `e8a7be4ce2` and re-record from that tree. Every golden's body is byte-identical to the one #21674 recorded; only the two header lines move: `baseline` (788 files) and `lockfileSha256` (778 files), the latter because #21652 changed `mobile/pnpm-lock.yaml` after that branch recorded. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb |
||
|
|
e8a7be4ce2 |
fix(omp): recover retired pane status with validated restart authority
Merged after fresh run 35448889017 passed all required checks, including static analysis, typecheck, package jobs, all test shards, changed E2E, Docker SSH E2E, and verify. |
||
|
|
e7da72c3d7 |
fix(omp): attach desktop and mobile images through file mentions
Merge fully verified: desktop/mobile focused suites, node and mobile typechecks, changed-code quality, hosted RPC recording pin, package checks, all test shards, and verify pass. This fixes #20389 across composer, drop, picker, and mobile clipboard-accessory paths. |
||
|
|
3ba7cb4de9 |
feat(diagnostics): trace terminal startup delivery phases
Merge fully verified: all required CI checks pass. This lands bounded startup timing instrumentation for the open Windows OMP first-paint investigation in #19333; it does not claim the latency fix itself. |
||
|
|
061a756b84 |
test(agent-status): pin that omp's approval_mode cannot hide a real prompt (#21499)
omp forwards its `approval_mode` on every `tool_approval_requested`, and the shared normalizer deliberately ignores it. Nothing recorded why, so the field reads like a dropped qualifier that a future change should start honouring. It must not be honoured. Measured against omp 17.0.5: the CLI emits this event only after its own policy engine already resolved the call to "prompt", and then parks on a human Approve/Deny select. Auto-approved calls emit nothing at all. `approval_mode` carries the ambient mode (always-ask | write | yolo), not the verdict, so a per-tool `tools.approval.<tool>: prompt` produces a genuinely blocked human carrying `yolo` -- the one value that looks auto-approving. No behaviour change. Records the reason at the decision site, replaces two fixtures that asserted an `approval_mode` of 'prompt' (not a member of omp's enum) with captured values, and adds guards pinning that every real mode, plus a missing or unrecognised one, stays blocked, and that pi is unaffected. |
||
|
|
b0ec11f5b0 | fix(omp): redact credential references before status transport (#21673) | ||
|
|
5cc89b6406 |
fix(sidebar): preserve OMP status across sparse split pane ids (#21671)
Use stable PTY-to-leaf bindings when in-session pane closes leave sparse runtime pane ids. Keep the existing parked and dense slot resolution paths, and cover the completed OMP pane/sibling-running case from #15557. Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> |
||
|
|
bf250b1200 | test(omp): prevent duplicate tab on worktree reselect (#21670) | ||
|
|
2e278426ef | fix(omp): sweep local agent PTYs during app quit (#21668) | ||
|
|
0e90e855db | Update README downloads badge | ||
|
|
5cf65d79fc | fix(omp): ignore nested task session ownership (#21663) |