mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 16:02:32 +00:00
bb77a2b1595ebbb65de29c73b98d68feba2dd71e
437
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
81ae98e10d |
fix(mobile): honor host worktree create retention (#16342)
* fix(mobile): honor host worktree create retention * fix(mobile): cover malformed worktree retention policy * fix(mobile): fail closed on malformed retention policy * fix(mobile): fail closed on missing dedupe ttl |
||
|
|
a724aa7b08 |
fix(mobile): report composing state from accessory backspace (#16757)
* fix(mobile): report composing state from accessory backspace The accessory path edits the field itself and then mirrors it, but called applyLiveInputMirror with two arguments where the signature takes three. The local option type declared it 2-ary, so the type checker never saw the drop. An omitted composing flag is not "not composing": it selects the Android-only heuristic that holds the trailing non-ASCII run. A pinyin preedit is plain ASCII, so the heuristic reads it as committed text and sends it. Typing `ni hao`, tapping accessory Backspace, then picking a candidate put `ni ha` on the PTY before the commit, giving `ni ha你好`. Korean survived this by accident - the non-ASCII heuristic re-derives the correct hold for Hangul - which is why it went unnoticed. The held range is the fact the mirror needs, and it is already in scope. Refs #13345 * fix(mobile): preserve accessory IME report provenance |
||
|
|
80246238fc |
fix(mobile): reset recycled paragraph layout before reuse (#16692)
* fix(mobile): quantize chat pinch font scale so a zoom stops re-measuring the list every frame
A user bubble on a 390pt iPhone painted five lines inside a frame that
reserved six, with the last painted line cut through a glyph at the content
edge and "no longer needed." gone.
The paint is React Native's: a `<Text>` with no `numberOfLines` gets a text
container whose `lineBreakMode` is `NSLineBreakByClipping`
(RCTTextLayoutManager.mm). Measure lays out into `{width, CGFLOAT_MAX}`, paint
lays out into the mounted content frame — so a frame one line short does not
re-wrap, it dumps the remainder onto the last fitting line and clips it, with
no ellipsis. Reproduced on-device against the real component with the message
text held constant, so five painted lines can only be truncation.
Two conditions are each necessary, and removing either makes it vanish over
~6000 measured bubble renders: a pooled `RCTParagraphComponentView` carrying a
shorter row's content frame (`prepareForRecycle` clears `state` but not
`_textView.layoutMetrics`), and whole-list re-measure churn while rows enter
and leave that pool.
The churn was ours. `renderItem` closes over `fontScale`, and the pinch handler
committed a new scale on every gesture frame, so one zoom drove hundreds of
full-list re-measures. The pinch is composed `Simultaneous` with the list's own
scroll, so a stray second finger during a scroll started that storm at scales
the user cannot see — matching the report, whose glyph metrics are `fontScale`
1.0 exactly.
`quantizeFontScale` snaps commits to a 5% grid. React bails out of a same-value
`setState`, so gesture noise now commits nothing and a full-range pinch commits
at most ~20 times. Under the churn that produced 67 defects in 6636 bubble
renders, the quantized build measured 0 in 6064 — with a forced-defect control
bubble flagged in 100% of frames of both runs to prove the detector was live.
This removes the trigger we own; it does not close the RN recycling window
itself. That needs a one-line reset in `prepareForRecycle`, which cannot land
here without refreshing the `patchedDependencies` hash under `mobile/`.
* fix(mobile): reset recycled paragraph layout before reuse
|
||
|
|
f6e16ba3f8 |
fix(native-chat): anchor an unmatched chat echo where it was sent (#16117)
* fix(native-chat): anchor an unmatched chat echo where it was sent The reported symptom was old user messages replaying below every new turn, so the conversation read as scrambled. The cause was not that the echo failed to match a transcript row. Claude consumes a mid-turn send through a `queued_command` attachment and writes no `type:"user"` record for it, so some echoes can never match, and no amount of matching will change that. The cause was WHERE an unmatched echo rendered: buildMobileNativeChatTransientData appended every pending item after the entire transcript, so it re-read below each turn that landed afterwards. Render each echo directly after the transcript row it was sent against, using the baseline the send already captures. An unmatched echo is then at worst a duplicate in the right position rather than a scrambled one, and it stays visible. Echoes sharing an anchor keep send order; a send with no baseline, or one whose anchor folding dropped, still falls back to the tail. Deliberately NOT fixed by deleting the echo. Inferring from send ordering that an echo can never match, then removing it, loses the user's own text for a message the agent did receive, and it cannot fire in the common case anyway - measured drain groups are 1,017 of size 1 against 55 larger. It also escalates an existing gap: the count pass has no baseline-tail guard, unlike the glue pass, while `messages` is a 40-row window that head-trims, resets on reconnect and grows at the front on loadEarlier, so a false landing there would license deleting a DIFFERENT outstanding message. That count-pass gap is real and left for a separate change; anchoring makes its worst case a duplicate in place rather than a scrambled conversation. * fix(native-chat): preserve folded echo anchors * fix(native-chat): preserve forward-folded echo anchors * fix(native-chat): keep leading folded echoes in place |
||
|
|
8d61cb8b77 |
fix(relay): survivable mobile pairing recovery + desktop assign rate gate (#16659)
* fix(mobile): retry the stored assignment when the director reports no newer move A director answering /v1/connect can only reply relay-moved with the stored assignment; it has no 'assignment unchanged' verb, and sticky assignments make equal-epoch replies the steady state. Treating every non-newer move as fatal made pairing recovery unwinnable for any transient cell dial failure (DRAINING, 1006), which bricked off-LAN pairing on Android 0.0.44. A non-newer move now confirms the stored assignment: the candidate re-dials it with a 250ms floor instead of abandoning the relay path. The move is never adopted or persisted, so the anti-rollback contract (requireStrictlyNewerEpoch for persisted moves) is unchanged. 4429 stays out of director recovery: each cell dial burns an invite attempt server-side and a director hop cannot relieve cell load. * fix(mobile): honor relay director Retry-After when pacing recovery Mobile /v1/resolve collapsed every non-OK status into a generic error and discarded Retry-After, so overloaded windows produced hammering instead of paced retries. RelayDirectorHttpError now carries status and retryAfterMs (clamped to 120s), and the reconnect controller floors its existing transport delay with it — no new timers or retry state. The Retry-After parser is extracted from the desktop relay client into src/shared and reused by both. * fix(mobile): attribute pairing log lines to their candidate path The pairing race interleaves the direct LAN and relay candidates into one PAIRING LOG pane; direct lines (WebSocket closed, Reconnecting 10.x.x.x:6768) carried no path label and repeatedly read as Relay retrying a private IP — misleading users and two investigations. The coordinator now wraps each candidate's sink with an idempotent Direct:/Relay: prefix at the one seam where both paths are known. * fix(relay): gate desktop /v1/assign at the per-host rate limit The director rate-limits /v1/assign per host at 5s, but every desktop retry path could fire immediately: both schedulers draw full jitter from [0, cap] (floor 0), first attempts after a drain are undelayed, the 400-fallbacks issue up to 3 assigns per round trip, and reconcile() cancels the armed Retry-After timer from ~8 refreshDemand callers. Production shows hosts permanently rejected at ~100-200 rejects per success. A shared per-host gate now lives inside requestRelayAssignment — the single assign call site — so every path books a >=5s (+jitter) slot. Retry-After raises the gate persistently, surviving the coordinator's timer cancellation. Concurrent callers serialize through a per-key chain. Callers with staleness fencing pass isCurrent; a superseded caller aborts after the wait instead of spending the host's slot. Internal 400-fallback retries stay one logical attempt and do not re-enter the gate. * refactor(mobile): rename the log-only assignment-echo predicate isCurrentAssignmentMove no longer gates control flow — every non-newer move retries the stored assignment — so the name overstated its role. * fix(relay): honor mid-wait raises and cap the assign gate's inline wait Review findings on the per-host assign gate: the deadline was read once before sleeping, so a sibling's Retry-After landing mid-wait was ignored (the exact storm the gate exists for), and the sleep was uncancellable — a booked five-minute Retry-After could park pairing IPC, which awaits reconcile inline, for its full duration. The wait now runs in 1s slices, re-reading the deadline and the caller's isCurrent fence each slice. Remaining waits beyond 15s fail fast as a RelayHttpError 429 carrying the remainder, so the existing schedulers pace with it while the gate keeps the deadline. Staleness aborts are classified non-retryable. Also from review: the broker's isCurrent wiring and the shared-gate default are now pinned by tests, the 4429 comment states the reservation-order rationale precisely, the mobile Retry-After ceiling is renamed to avoid colliding with the desktop's 5-minute one, and a past-HTTP-date header case is covered. * fix(relay): tag locally paced assigns and warn about frozen test clocks Review polish: the synthesized 429 for a beyond-cap local wait now carries a distinct message (relay_assignment_locally_paced_429) so log censuses can tell it from a real director 429, with the comment stating the invariant that makes the translation honest (local booking alone never exceeds ~5.5s). The gate's sleep option documents that test fakes must advance the clock — the slice loop re-reads it and never terminates against a frozen one. * fix(relay): fence superseded callers at the assign send boundary reserve() checks staleness while waiting, but a caller superseded after booking — or between the 400 field-fallback retries — could still spend one to two requests on an assignment nobody consumes. Re-check isCurrent at the top of sendRelayAssignment so the fallback recursion is fenced too. |
||
|
|
588eec68b4 |
fix(native-chat): stop rendering a tool result whose call is outside the window (#15653)
* fix(native-chat): stop rendering a tool result whose call is outside the window A tool result carries no call id, so it can only be attributed to a tool call loaded alongside it. Both chat views read a windowed transcript tail (mobile 40 messages, desktop 300), and the window regularly opens between an assistant's `tool_use` record and the user-role record that answers it. Claude also re-emits already-answered `tool_result` records at a `/compact` boundary, long after their call scrolled out of the window. `foldToolMessages` had no rule for those: with no assistant predecessor in the output they were pushed through as standalone messages and rendered as a bare, unowned block of raw tool output with no tool name — reading as a message from nowhere mid-conversation. Sampling real Claude transcripts, 176 of 400 sessions (44%) produced one in a mobile-sized first page. Drop a result no loaded call can own, before folding. It is not lost: it comes back attached to its call as soon as the owning turn pages in. * fix(native-chat): scope tool result attribution to folded turns * fix(native-chat): preserve harness-attributed tool results * fix(native-chat): keep interruption boundaries |
||
|
|
4ff428f763 | fix(mobile): retain relay during brief backgrounding (#16543) | ||
|
|
d2a35eebe3 | fix(mobile): avoid unsupported Hermes array sorting (#16506) | ||
|
|
0e0a8c943b |
fix(mobile): recover Relay connections after resume (#16498)
* fix(mobile): recover relay sessions on resume * fix(mobile): expedite relay retry on app resume * fix(mobile): keep relay reconnect controller under lint limit * fix(mobile): rebuild relay client after pairing rejection * refactor(mobile): keep relay reconnect policy under lint limit |
||
|
|
efa3b972c2 | fix(native-chat): prevent duplicate mobile prompt echoes (#15656) | ||
|
|
98bdd653ab |
fix(native-chat): stop the spinner on a not-yet-flushed transcript (#16493)
* fix(native-chat): stop the spinner on a not-yet-flushed transcript A brand-new agent session can take minutes to write its first JSONL line, and one that is never prompted never writes it at all. The host emitted no stream frame until the file resolved, so every native-chat client sat on a bare spinner with the composer enabled but the transcript blank -- forever, in the never-prompted case. The resolve poll now reports the transcript as pending after a short grace, and both host handlers emit a `pending: true` snapshot. It is deliberately not a plain empty snapshot: an empty window sold as a settled read would capture over retained history and unblock consumers that require a trustworthy transcript (the launch-draft adoption would re-offer a prompt the agent may already have taken). Clients render it as the "start a chat" empty state while keeping the read unsettled -- `awaiting-transcript` on mobile, an `awaiting` read phase on desktop, which also stops the seed loop expiring into an error card for a session that is simply new. New optional field only, so older clients ignore it and still stop spinning. * fix(native-chat): negotiate pending transcript frames |
||
|
|
aa4c9c707c |
Refactor mobile home, worktree modal, and RPC client (#16165)
* refactor(mobile): split home modal and rpc client * fix(mobile): restore render-phase remount key in NewWorktreeModal The split moved the form-reset epoch from render-phase refs into useState + useLayoutEffect, which changed when the remount key is computed. On the render where visible flips false->true the key was still the old epoch, so the previous session's NewWorktreeModalContent rendered with visible === true carrying stale form state. Child layout effects run before the parent's, so visible-gated hooks (useNewWorkspaceRepositories, useNewWorktreeDrawerNavigation, useNewWorkspaceRuntimeContext) fired for that stale instance before the parent bumped the epoch and remounted. Restore the ref-based computation so the key is correct on the first render where visible flips true, keeping the composite open/client epoch semantics and the file split intact. |
||
|
|
4da8848168 | fix(mobile): preserve relay close recovery codes (#16293) | ||
|
|
ec4687c434 |
feat(agents): distinguish Claude background monitoring (takes over #14205) (#16201)
* feat(agents): distinguish Claude background monitoring Adds an optional `workingMode: 'monitoring'` discriminator for a Claude session whose lead turn finished but which still has background shell tasks or session crons registered. The wire state stays `working`, so older peers that never read the field keep rendering Working. (cherry picked from commit |
||
|
|
21c134bb66 | refactor(mobile): reuse repository color helper (#16131) | ||
|
|
b2902cb61e |
fix(agent-resume): restore Kimi Code sessions after restart (#15883)
Co-authored-by: Melih <mberatsanli@gmail.com> |
||
|
|
8bf1128a08 | fix(mobile): remove close-tabs-to-right action (#15894) | ||
|
|
30bf2647fc |
fix(mobile): replay a delivery-ambiguous worktree.create instead of failing it (#15472)
* fix(mobile): replay a delivery-ambiguous worktree.create instead of failing it A socket close or response timeout rejects an in-flight worktree.create as delivery-unknown: the frame reached the wire, so the host may already have built the worktree. The client only replayed connection-migration cutovers, so every other ambiguity surfaced as a create failure for a create that may well have succeeded. Replay on the same clientMutationId — which the host already dedupes — after waiting for the transport to come back. * fix(mobile): bound the ambiguous worktree.create replay by the host's dedupe window The replay was bounded only by a retry count, but what makes a replay reconcile instead of building a second worktree is wall clock: the host drops a settled create's dedupe record 60s after it resolves, and past that the replay is just a fresh create that the host's suffix loop happily duplicates — for a folder workspace, into a second workspace with the very same name and no collision check at all. Two paths ran past that window: - The request-timeout path. A silently dropped response frame leaves the socket alive, so nothing rejects until WORKTREE_CREATE_TIMEOUT_MS — ten minutes, with no bound at all on when the host actually resolved. This was previously the path that replayed *soonest*, short-circuiting the reconnect wait because the transport still looked healthy. Invert it: every path that reports a real drop has already left 'connected' by the time the rejection surfaces, so still being 'connected' identifies the timeout and is now refused. - The reported-drop path. Worst-case detection is a full liveness idle period plus the missed-probe budget before the client even learns the socket is dead, and the old 20s wait on top of that overran the record. Derive the wait from the watchdog constants and the TTL instead of hardcoding it, and anchor a single deadline at the first ambiguity so a second wait gets the remainder rather than restarting. The TTL now has one definition shared by both processes, so the client asserts its budget against the host's real window instead of a copied literal. * fix(mobile): end the reconnect wait on a revoked pairing, and pin the wait's behavior waitForRpcClientReconnected resolves only on 'connected' or the timeout, but an 'auth-failed' client never reaches 'connected' — so a create interrupted by a revoked pairing sat out the full wait before surfacing the error it already had. Treat auth-failed as a terminal answer on both the fast path and the listener. The helper also shipped with no tests of its own: its already-connected fast path, its timeout path, and the synchronous-notification-during-subscribe teardown were only ever exercised indirectly through the retry suite, and neither RpcClient implementation notifies synchronously, so that branch had no coverage at all. Add a direct suite covering all of them, asserting listener and timer teardown rather than just the resolved value. Also give the fake-timer tests an explicit timeout. advanceTimersByTimeAsync yields through real macrotasks between ticks while vitest's own budget runs on real time, so on a loaded runner the default 5s is reachable — observed once as a spurious timeout in this suite. * fix(mobile): bound the ambiguous replay in wall clock, not timer time The replay window was derived from the liveness watchdog's own budget (idle + missed probes x probe timeout). That is a bound on how long the watchdog takes to *fire*, not on how much wall clock passed. iOS and Android suspend JS timers while the app is backgrounded, so across a background cycle the socket dies silently and the pending create rejects delivery-unknown minutes later with the timer-derived ceiling still reading ~44s. The replay then lands well past the host's 60s dedupe record and the suffix loop builds a SECOND worktree - for a folder workspace, one with the very same name and no collision check at all. Anchor the deadline on the watchdog's lastInboundAt instead: a wall-clock stamp of a frame that really arrived, so it stays honest across a suspension. Fall back to the send time when the transport can't vouch for one (relay sessions run with idleProbeMs: null), which errs toward refusing the replay. Also restore the delivery-unknown discrimination test that the still-connected guard had made vacuous, pin the still-connected guard itself against a live inbound stamp, and pin the deadline against being re-read from a fresher replacement session. |
||
|
|
fab6e0d6e7 |
fix(mobile): scope optimistic workspace removal to the deleted host (#15424)
* fix(mobile): scope optimistic workspace removal to the deleted host A worktreeId repeats across hosts, so filtering the list on the bare id also removed the identically-named workspace belonging to the other host. Match on (worktreeId, hostId) through a named helper so the rule is testable. * fix(mobile): key host worktree rows consistently |
||
|
|
bb09dc1749 | fix(mobile): escalate a persistently rejected Relay pairing to re-pair (STA-4681) (#15237) | ||
|
|
0e96b82e44 |
fix(mobile): keep phone tab selection across host snapshots
* fix(mobile): keep phone tab selection across host snapshots Preserve device-owned tab focus across ordinary host republications while explicit follow navigation remains authoritative. Retire closed selections across clients so stale snapshots cannot resurrect tabs. * fix(mobile): acknowledge session tab closes * fix(mobile): avoid tombstones for uncommitted closes * fix(web): implement session close IPC stubs * refactor: simplify mobile tab close flow * fix: bound session tab close confirmation |
||
|
|
b0e27354b5 | fix(mobile): escalate continuous Relay outages (STA-4587) (#15071) | ||
|
|
c7995a66ae |
fix(mobile-native-chat): reland glued pending retirement without the two revert causes (STA-4482, STA-4492) (#14936)
* fix(mobile-native-chat): reland glued pending retirement without the two revert causes Relands #14665 (reverted by #14819). #14665 retired mobile pending bubbles when two fast sends landed as one transcript row, but shipped two regressions; both are fixed here rather than re-applied and hoped for. 1. A rejected send restored a TRIMMED composer. #14665 reassigned `text` to `text.trimEnd()` at the top of `sendMessage` and then used that one value for both the bytes on the wire and the composer restore, so a rejection put back less than the user typed. The draft and the payload are now separate values: `draftText` is what the user typed and is what `clearDraftForSend` / `restoreRejectedDraft` see; only the transported `text` is trimmed. 2. Sends issued during hydration were stranded forever. #14665 persisted `glueBaselineTrusted: false` on any send captured while the transcript was still loading and never cleared it, so that send could never retire and stood as a permanent glue barrier for its neighbours. A hydration-time baseline is now a placeholder (`baselineResolved: false`) that the first authoritative read rebases onto real rows, ordinals included, instead of a permanent disqualification. That is STA-4492. The intended behavior is unchanged: one transcript user turn retires a run of 2+ adjacent text-only pending sends only when it exactly spells their normalized concatenation, every send is bounded by its OWN transcript tail, and exact landings, image echoes and unresolved tails stay barriers. No wire change: `baselineResolved` and the baseline tail are client-local React state in `pendingBySession` and are never exchanged with a host. The only client->host difference is trailing whitespace no longer being written onto the agent's input line, over the existing `terminal.send` params. Refs STA-4482, STA-4492. Original PR #14665, revert #14819. * fix(mobile-native-chat): let the untrimmed draft reach the send seam The composer sent `value.trimEnd()`, so the raw draft never reached `sendMessage` and a rejected send still handed back a trimmed composer — the split of `draftText` from the transported `text` had nothing to restore. Pass the draft through; the seam already owns the wire trim. Also pins the array-identity contract of `retireLandedMobileNativeChatPending`: the drafts effect early-outs on `next === current`, and nothing tested it. * docs(mobile-native-chat): name the hydration rebase's residual ambiguity * fix(mobile-native-chat): stop the hydration rebase stranding a send on its own echo Rebasing recounted the send's ordinal against the first authoritative read. That read can already carry the send's own echo — a re-subscribe after a tab switch or reconnect returns whatever exists now — so the ordinal landed one past anything the transcript could supply. The bubble never cleared, it stayed a live segment at the head of its run so no later pair could glue either, and `earlierOutstanding` carried the inflation onto the next send of the same text. Only the tail needs recovering; the ordinal was already counted against an empty transcript, which is right for "no history was known". A caption-less image echo keeps its captured tail, since it counts turns after it. `baselineResolved` also has to mean "captured against a settled read", not merely "not loading": a read that failed hands back an empty list that reads as an empty conversation, and the null tail then let any row the successful read finally brought glue-retire those sends. * test(mobile-native-chat): pin that a resolved hydration send leaves its run glue-capable A held send sits as a live segment at the head of its run, so the cursor can never reach a later pair — the stuck bubble takes the whole feature down with it. Goes red against the ordinal recount. * fix(mobile-native-chat): pin an image echo that captured no tail, and require the settled flag A caption-less image echo keeps its captured tail because it counts image turns after it — but a send issued before any history was known captured null, which counts from the top of the transcript. An old image turn then claimed the send and bound the user's fresh photo to it, leaving the just-sent turn with no preview. A null tail is not a boundary worth preserving, so pin those too. `transcriptSettled` was optional and defaulted to the gate it replaced, so any caller that omitted it silently got the pre-fix behaviour. Required now, and threaded through every harness. * fix(mobile-native-chat): stop an unbounded send claiming an image turn already in the read The image-preview pass runs before the rebase, so a send captured with no boundary matched any image turn the settled read carried — binding the user's freshly attached photo to an old one and retiring the bubble through landedImagePendingIds, which short-circuits the retirement path entirely. Pinning the tail in the rebase could not help: the claim was already made. Such an entry now waits one tick and claims against a real tail. * fix(mobile-native-chat): never move a boundary the send already captured An unsettled read still shows this session's own retained history — a reconnect or a failed read keeps the conversation on screen rather than blanking it — so sends made across one already own a correct tail. The rebase overwrote it with the tail of the read that followed, which sits at or after their own glued row, so `turn.index <= segment.tail` rejected every turn and the pair stayed queued for the session, blocking every later pair in the run. Pin only a send that captured no tail at all. A captioned image echo is now left alone entirely: it binds its preview by an ordinal counted over the whole transcript, so supplying a tail without recounting left it matching nothing, forever. * fix(mobile-native-chat): supply a boundary only to a text-bearing send An image echo reconciles by counting turns AFTER its tail and has no other retirement path, so the tail supplied from a read that already carried its own echo excluded the very row it was waiting for: the "Queued" photo bubble stuck for the life of the session and the transcript row rendered as bare marker text with no photo. A regression against main, and against the earlier revision of this fix that pinned only captioned echoes. The glue matcher is the only consumer a supplied tail helps. Everything that reconciles relative to its own tail keeps whatever it captured. * fix(mobile-native-chat): stop one unmatchable send freezing glue for the session The match cursor only advanced on a hit, so a head that could never match — a pair whose glued row arrived with the read, or a send the count pass claimed against an older row — froze the run behind it and every later rapid pair became permanently unretirable. Two cases previously disclosed as bounded were not bounded at all. Slide past a non-matching head, keeping the cursor monotonic so a later turn can never take a send an earlier one claimed. The slide widens the search, so a span cap keeps the work linear in the run length instead of quadratic; the existing budget test now asserts that bound rather than the old one it silently broke. Re-fuzzed at 250k seeds: the boundary guarantee still holds. Also corrects a comment that claimed the preview-pass filter made a photo claim against a real tail. It does not — an image echo keeps whatever tail it captured, so a caption-less photo can still bind to an older photo turn, as on main. * fix(mobile-native-chat): stop the span cap stranding a long glued run Capping each match attempt at 8 segments did not truncate a longer glue, it rejected it outright: a row spelling 9+ sends exhausted the loop without reaching the end of the text and returned zero, so none of the nine retired — and each stuck send then inflated `earlierOutstanding` for the next send of the same text. Nothing bounds how many sends pile onto the agent's input line; accumulation ends when the agent accepts input again, not at any fixed count. One inspection budget now covers the whole slide instead. The first attempt spans the entire run and always fits, so a genuine glue is never truncated; only a run of identical prefix-matching sends can exhaust the budget, which is exactly the case that should be cheap. The in-flight attempt may overshoot the remainder — that is what makes the guarantee hold — so the budget test asserts the real ceiling. Re-fuzzed at 250k seeds with runs past the budget. |
||
|
|
66b599399f |
fix(mobile): decide terminal preedit from the marked-text range, not a script table (#15007)
* fix(mobile): decide terminal preedit from the marked-text range, not a script table The live terminal capture field decided what to withhold from the PTY with a Unicode-block allowlist (Hangul jamo and syllables) and held exactly one trailing code point. Kana and kanji are not in the table, so a Japanese reading streamed to the PTY one fragment at a time and was repaired afterwards with DEL bytes (#7427). A code-point table cannot work, and the counterexample is not exotic: Chinese pinyin preedit is plain ASCII, and a Japanese romaji reading is one code point on the first keystroke and three on the fourth. Preedit is a property of the FIELD, not of the characters in it, so the only signal that identifies it is the text system's marked-text range. That is what a reference terminal implementation uses on every platform it supports - `hasMarkedText` there, the input-method context's composing state elsewhere - and neither one classifies code points anywhere in the input path. So the mirror now takes the marked-text report per change and holds the whole preedit region, whatever its length or script: - Subscribe the capture field to `onChange`, not `onChangeText`; only the raw native event carries the report at all. - A reported preedit is held entire and is never committed by the settle timer, because preedit is not text yet. Explicit boundaries still flush it. - `isTerminalLiveHangulCodePoint` and its four ranges are deleted. iOS reports the range but React Native drops it before JS, so the pinned patch forwards `markedTextRange` into the change payload. It is three hunks and it compiles because the app already sets `buildReactNativeFromSource` for iOS. The same idea was proposed in #11450, which is where the patch comes from. Android has no marked-text report in React Native at all, and a Kotlin patch would not help: Android consumes the prebuilt react-android artifact, so node_modules sources are never compiled. Until the report exists there, the fallback holds the trailing non-ASCII run. It enumerates nothing, it covers kana, kanji and Hangul, and ASCII keeps its zero-latency echo - but it cannot see an ASCII preedit, so Chinese pinyin on Android still leaks its reading. Only a report fixes that. Not-tested: no physical device or emulator was available, so no real IME drove this path. Japanese, Chinese and Korean composition are covered at the model and hook level only, and the iOS patch has not been compiled. Co-authored-by: Brennan Benson <brennanb2025@users.noreply.github.com> * fix(mobile): bound the fallback hold to text the pty has not received The no-report branch walked the trailing non-ASCII run over the whole field and ignored stableLength, unlike the reported branch directly above it. So after a settle-timer commit the next keystroke re-held everything already delivered and the caller erased it with DEL and retyped it — a nine-character Cyrillic word cost a DEL per already-sent character, and for the 300ms before the re-send the held text was the only copy, so a blur or reconnect destroyed characters the pty already had. Bound it the way the reported branch is bounded. Pinned by a test that drives a settle commit between every keystroke and asserts no DEL reaches the wire. --------- Co-authored-by: Brennan Benson <brennanb2025@users.noreply.github.com> |
||
|
|
b6d5972ec4 | fix(mobile): reland truthful Relay recovery status (#14986) | ||
|
|
a324ee20d4 |
Reset terminal SGR state around restored output (#14700)
* fix(terminal): reset SGR around restored output * fix(terminal): preserve live replay styling * fix(terminal): ground dead reattach fallback |
||
|
|
c73e5a2f59 |
fix(mobile): bound pending-handle session-tab recovery polling (STA-4407) (#14916)
* fix(mobile): bound pending terminal recovery polls * wip(mobile): partial STA-4407 bound pending-handle poll * fix(mobile): finish bounded pending-handle recovery * fix(mobile): preserve pending-handle recovery attempts on slow links * fix(mobile): retain pending-handle cadence budget semantics * test(mobile): pin pending recovery parked state resets * chore(mobile): drop the STA-4407 worker status logbook * fix(mobile): preserve pending recovery liveness * fix(mobile): coalesce repeated recovery retries * fix(mobile): memoize pending recovery context * fix(mobile): type the pending recovery poll test renderer explicitly * fix(mobile): type the poll test renderer without an any union * fix(mobile): keep recovery context refs current * test(mobile): tighten pending recovery coverage * test(mobile): preserve recovery-source liveness * fix(mobile): keep the poll test renderer union free of any * test(mobile): prove parked recovery isolation * fix(mobile): write the parked-recovery callback ref after commit * test(mobile): isolate recovery identity changes * test(mobile): prove recovery publication boundaries |
||
|
|
1e63cfef06 |
Revert "fix(mobile): present pending Relay fallback accurately (#14922)" (#14976)
This reverts commit
|
||
|
|
3811881410 | fix(mobile): present pending Relay fallback accurately (#14922) | ||
|
|
b8dc393c18 |
Revert "fix(mobile-native-chat): retire pending bubbles glued into one transc…" (#14819)
This reverts commit
|
||
|
|
7aaa7c6f5b |
refactor(sidebar): group worktree-list files by domain (#14486)
* refactor(sidebar): group worktree-list files by domain Follow-up to #14465 / #14467. Keep the landed extract and reorganize the flat worktree-list dump into drag/, headers/, reveal/, rows/, scroll/, and viewport/. Fold tiny modules into their owners, move leftover sidebar-root files into the module, and retarget imports and source-path tests. Layout-only; no behavior change. * fix(sidebar): merge duplicate virtual-rows imports Inlining virtual-row-dom-attributes left a second import from the same module, which fails audit:code-quality:native --deny-warnings. * refactor(sidebar): condense indentation comments Shorten explanations to focus on the essential why, removing redundant detail and improving readability without changing functionality. * refactor: organize worktree-list into lifecycle dest folders * fix react doctor * fix: update reliability-gates path after worktree-list reorg host-filtering.test.ts moved from viewport/ to listing/; keep the runtime-routing.active-server-preference gate pointing at the real file. * Extract workspace status colors to design tokens Define theme-aware color tokens for workspace PR-state indicators (done, in-review, in-progress) to ensure consistent identity across theme switches. Update references to use the new tokens and refactor EmptyState button to use the Button component. * fix(sidebar): stop mutating refs during worktree-list render React Doctor fails static analysis when refs are written in render. Commit reused array identity and the Smart live-signal latch after paint, and return the attention map from the sort memo instead of stashing it on a render-time ref. |
||
|
|
68ca17e46c |
fix(mobile-native-chat): retire pending bubbles glued into one transcript row (mobile half of #14262) (#14665)
* fix(mobile-native-chat): retire pending bubbles glued into one transcript row Mobile's native chat retires an optimistic pending bubble only when a transcript user turn matches its normalized text at the expected ordinal. When two rapid sends collapse into a single glued user row neither key matches, so both bubbles pin below every newer reply for the rest of the session — mobile has a parallel implementation with no glue handling at all. Trim the send body once at the send seam so the bytes the host writes verbatim and the reconciliation key describe the same message on every send path, then add a bounded glue matcher: a greedy cursor walk that may only consider transcript turns strictly AFTER each send's captured tail, so an older turn that happens to read like the concatenation can never retire a newer queued send. Refs #14262 * fix(mobile-native-chat): harden glued pending retirement * fix(mobile-native-chat): preserve pending image previews * fix(mobile-native-chat): bound glue to loaded transcripts |
||
|
|
ab9d1a29a9 |
fix(worktree): never reissue a generated workspace name (#14350)
* fix(worktree): never reissue a generated workspace name
Generated workspace names were deduped only against currently-live
worktrees, so deleting a workspace returned its name to the pool. A later
workspace could draw the same name, land on the same directory path, and
inherit the previous occupant's agent conversation history — coding-agent
CLIs key their prompt history and transcripts by cwd.
Names are now retired permanently per repo. The registry is written in
main with the name Git actually used (the create loop can advance past a
requested name on collision), and seeded once per run from workspace
directories and surviving agent transcript buckets so already-spent names
are excluded from the start. Suggestions degrade to -2, -3 variants
instead of recycling, and those variants retire too.
User-typed names are untouched: retirement filters suggestions only.
* fix(mobile): honor retired workspace names, on one shared implementation
Mobile hand-duplicated the desktop name-suggestion algorithm and deduped
only against live workspaces, so a phone could still be offered a name
whose deleted workspace left agent conversation state behind at that path.
Both platforms now call one shared selector in src/shared, so the two can
no longer drift. The host publishes retired names as an optional field on
the existing worktree.list response, and mobile fetches them per selected
repo while the create sheet is open — mirroring the desktop hook.
Mobile never calls worktree.list for its catalog (it uses worktree.ps,
which carries rows only), so this is a targeted request rather than a
change to the catalog or its cache. Hosts predating the field omit it and
mobile falls back to live-only dedupe, which is the pre-change behavior.
* fix(worktree): close retirement consistency gaps
* test(worktree): cover retirement runtime contracts
* fix(worktree): retire generated collision names
* fix(worktree): enforce retired names at creation
* refactor(ai-vault): extract the Claude project-dir encoder
The bucket-name encoder and its scope-boundary check were private to the
session scanner, so a second consumer had to reimplement them — and got the
per-character encoding wrong. Move both to a shared module with direct tests.
* fix(worktree): make the retirement seed scan actually match buckets
The bucket encoder collapsed runs of non-alphanumerics while the real one
emits a dash per character, so every dot-path bucket missed and the Windows
default workspace root (C:\...) matched nothing at all. Reuse the shared
encoder and its boundary check, which also stops a repo absorbing a sibling
whose path merely shares its prefix.
Also:
- Derive the workspace leaf by stripping the known encoded parent instead of
guessing from trailing dash segments, which retired the parent directory's
name whenever a workspace was named numerically.
- Reuse isAutoGeneratedCreatureBranchName so the -10 and -100 tiers retire.
- Drop the .codex/sessions root: Codex keeps the cwd inside the transcript
rather than in a directory name, so the scan could only ever see a year
folder. Reading transcript contents is not a trade this feature justifies,
so the gap is documented instead.
- Honor CLAUDE_CONFIG_DIR, which relocates the bucket root.
- Delete the unused retirableLeafName export.
Tests write buckets with the real per-character encoding against a fake home,
covering POSIX, dot-directory, Windows drive and WSL UNC roots; all three
platform cases fail against the previous encoder.
* fix(worktree): retire only generated names, keyed by cwd namespace
Two problems in the host-side registry.
Retirement fired for every create, including names the user typed. The
creature pool contains ordinary words — orca, runner, sole, molly, oscar — so
typing a retired 'nautilus' silently produced directory and branch
'nautilus-2' and burned the name for good. Creates now carry an explicit
nameWasGenerated flag; both the skip and the retire are gated on it, and it
defaults to false so CLI and automation callers are unaffected.
The registry was keyed by repo id, but both readers already discarded the id
and unioned by the cwd collision key, because the collision this prevents is
on the path. Keying by that namespace directly fixes several things at once:
entries no longer orphan when a repo is removed, remove/re-add no longer loses
every retirement for an unchanged path, the missing removeProject prune is
moot, and the backfill promise no longer merges into only the first repo id it
saw. The feature is unreleased, so no migration is needed.
Also:
- Memoize the collision key. It runs computeWorktreePath, which for a WSL repo
is a blocking execFileSync('wsl.exe') whose failure path is uncached, and
the previous code recomputed it once per repo on every create and every
listRetiredNames call.
- Drop retiredNamesByRepo from the worktree list result. It had no readers and
leaked onto 'orca worktree list --json', and its awaited backfill sat on CLI
selector resolution. The dedicated listRetiredNames RPC keeps its consumers.
- Make the three RuntimeStore methods required. RuntimeStore is file-private
with two constructors, so the 'older embedders' the optionality protected do
not exist, and the optional chain silently returned no retirements.
- Revert the unrelated forceDeleteBranch rewrite, and make room under the
file's line budget by extracting the create-args mapping instead.
* fix(worktree): send name provenance and stop gating Create on the fetch
Desktop and mobile now mark a create as generated-name only when the user
typed nothing and the composer fell back to the suggestion, so the host knows
which names it may retire.
Remove the retired-names loading gate from every create path. The host already
skips retired candidates before doing any git work, so the client gate bought
nothing while it could disable Create for the length of a full mobile
reconnect ladder (the wait had no timeout) and blank the desktop button
between queued creates. The suggestion still waits; the button never does.
Also make the web client call worktree.listRetiredNames instead of hardcoding
an empty list — the method is registered and mobile-allowlisted, so the
comment claiming no wire call existed was wrong — and filter the mobile
response to strings so a malformed row cannot throw during normalization.
* fix(worktree): key retirement by repo id and prune it with the repo
Reverts the collision-key storage key. It was a function of workspaceDir,
nestWorkspaces, worktreeBasePath and repo.path, so toggling any one of those
orphaned every retirement for every affected repo at once — trading a rare
churn (remove/re-add) for a common one. The read path already unions by cwd
namespace at query time, so cross-repo sharing never depended on the storage
key.
Instead, address the growth and orphaning directly:
- Drop the registry in removeProject, and in removeProjectForHost once the last
host's copy of the repo id is gone, alongside the sparse-preset deletes that
already follow this convention.
- Bound each repo's registry. The cap sits far above the 552-name pool because
evicting inside it would reissue a name whose agent state is still on disk;
only -2/-3 tier accumulation can ever reach it.
- Carry retirements through profile transfer, re-keyed to the destination repo
id and dropped from the source, mirroring sparsePresetsByRepo.
Separately, fix the backfill merge: the scan promise is cached per cwd
namespace, but it closed over the first repo id that triggered it, so a second
repo in the same namespace received nothing. The scan stays shared; the merge
moves out of the cached promise and runs for whichever repo asked.
Local repos re-seed on re-add through that backfill. SSH repos do not — the
scan cannot see the execution host — which is now stated in the module.
* docs(worktree): spell out why the retirement bound sits above the pool
Names the trap directly: the neighbouring 50/200 bounds cap histories, so
lowering this one to match them would silently start reissuing names whose
agent state is still on disk. Also states that oldest-first eviction is a
deliberate least-bad choice rather than a neutral one.
* fix(worktree): send name provenance from the web runtime client
This client hand-enumerates worktree.create params, so the new optional field
was silently dropped and typecheck could not see it. On web and paired-desktop
the host therefore never received it: generated names were never retired, and
the host-side skip that backstops a stale suggestion was disabled too. The same
client does fetch retired names for suggestions, so it was filtering against a
registry nothing ever wrote to.
The test asserts both directions, and fails without the fix.
* fix(worktree): retire names that took more than one collision suffix
isAutoGeneratedCreatureBranchName strips exactly one trailing -N, which is
right for auto-rename eligibility but wrong here. Once the pool is spent the
suggester emits nautilus-2, and a collision on that yields nautilus-2-3 —
which a single strip leaves as nautilus-2, not a pool name, so retirement
no-opped at exactly the tier where every base name is already gone. Strip
repeated suffixes locally rather than moving the auto-rename predicate.
* perf(worktree): keep the retirement backfill off the blocking WSL probe
The backfill runs on composer repo-select, not just at create time, and it
derived the probe path synchronously — which for a WSL repo with a mirrored
workspace dir reaches getWslHome and its blocking execFileSync('wsl.exe').
A stopped distro froze the main process for up to 5s on composer open.
Adds an async twin of computeWorktreePath and uses it for the probe. Resolving
the home there also warms the shared cache, so later sync callers are free.
Also stops memoizing the collision key when the WSL home is still unresolved:
only the success path is cached upstream, so caching the fallback namespace
would strand the repo there for the rest of the session.
* fix(worktree): hold retired names across a refresh instead of blanking
refreshKey changes on every workspace-list mutation, so create-multiple
refetches after each create and the hook returned an empty list until the
refetch landed — precisely the window in which resetForNextCreate clears the
name field and a fresh suggestion is drawn. Keep the previous answer while
revalidating and reset only when the repo changes; a failed refresh keeps what
was already loaded rather than un-retiring everything.
Also makes the returned array referentially stable, so the suggestion memo
downstream stops rerunning on every refetch.
* refactor(worktree): put the retired-name cache rules on one implementation
The desktop and mobile hooks that fetch retired names had already drifted
four ways. The transports genuinely differ (IPC vs RPC), but the caching
rules must not, and mobile's copy reset to [] on any error -- which
un-retires every name for the rest of the sheet session, the one outcome
retirement exists to prevent.
Moves the rules into src/shared/worktree/retired-name-cache: response
normalization, the never-leak-across-repos rule, and the hold-previous-on-
failure rule. Pure, no React, because src/shared is on the main process's
import graph. Each platform keeps its own transport and effect.
Mobile moves up to desktop's behavior: it now holds the previous answer
through a failed refresh, and refetches when the workspace list changes
instead of never refetching after mount.
Also drops the unused `loading` return. Neither platform consumed it; its
only consumer was the Create-button gate reviewed out earlier, and removing
it makes that regression unexpressible.
* fix(worktree): import shared types from their real modules
Main dropped the src/shared/types barrel, so the retirement module's import
resolved locally but not against the PR's merge base.
* refactor(worktree): bound the retirement registry by tier compaction, not eviction
Retirement is a correctness guarantee — a spent name's directory may still hold
agent conversation state keyed by that cwd — so the 2000-entry cap was the wrong
shape: reaching it handed a name back. At the owner's measured rate (~6.6 pool
names retired per day in one repo) the cap was ~9 months out.
Names come from a fixed 552-entry pool and the suggester only reaches tier N+1
once every tier-N name is taken, so a completed tier is exactly a set that no
longer needs listing. A row is now a watermark plus the names above it: reads
answer at-or-below the watermark with no lookup, and compaction drops the 552
entries the watermark now covers. Bounded at one pool per repo forever, with no
eviction and nothing un-retired.
Tiers can complete out of order (a create-time collision can spend `nautilus-2`
while tier 1 is open), so compaction loops and higher-tier names simply wait.
The RPC result carries the watermark beside the names as a new field; a client
predating it reads the names only and under-retires the compacted tiers, which
degrades to the pre-retirement behavior rather than breaking.
* fix(worktree): preserve generated name retirement across failures
|
||
|
|
e570cade3c |
fix(mobile): keep polling session tabs while an active terminal is pending-handle (STA-4256) (#14623)
A terminal tab published as `status: 'pending-handle'` renders the session screen's spinner. Leaving it requires a snapshot that carries the materialized handle, but a certified-live tabs stream parks `poll()` unless `hasRecoveryNeed()` says otherwise — and that predicate never considered a pending terminal. A host that mints the handle without republishing therefore stranded the pane on its spinner forever: measured live, zero further `session.tabs.list` calls over 90s while `terminal.list` kept firing every 2s. Mirrors the existing native-chat recovery-need pattern. Client-only; no wire change. |
||
|
|
7558fb064a |
fix(browser): keep browser guests painting when the workbench is hidden (#14599)
* fix(browser): keep browser guests painting when the workbench is hidden
Chromium never paints inside a display:none subtree, so an Electron <webview>
stops emitting CDP screencast frames the moment any ancestor is parked that way.
Orca already models this per pane (browser-page-paintability.ts) and per worktree
surface, using opacity:0 so a phone- or agent-driven page keeps compositing — but
three ancestors above those layers still used `hidden` unconditionally:
- the App-level terminal workbench container, hidden whenever activeView is not
'terminal' (opening Settings froze every mobile browser pane),
- Terminal's root, hidden when there is no active worktree,
- the split-surface wrapper, hidden when the active worktree has no layout.
A pane-level escape hatch cannot override an ancestor, so all of them have to
agree. Share one predicate across the chain and swap `hidden` for an out-of-flow
transparent layer while a remote controller needs frames.
The predicate ORs automation visibility with the mobile driver, matching the
per-worktree gate. That term is load-bearing, not symmetry: agent-browser
commands acquire a visibility lease and then capture, so gating on the mobile
driver alone left automation from a non-workspace view capturing a blank surface.
Mobile: a stream can report `ready` and then deliver no frames, which cleared the
loading indicator and left an unexplained black rectangle. Key it off actually
having pixels. That also retires the `ready` state and its ref.
Co-authored-by: Kaylee Williams <65376239+KayleeWilliams@users.noreply.github.com>
* fix(browser): keep paint retention off store hot paths
---------
Co-authored-by: Kaylee Williams <65376239+KayleeWilliams@users.noreply.github.com>
|
||
|
|
83e2123582 |
Add global worktree visibility source defaults (#14276)
* Add global external worktree visibility defaults * Expand global worktree visibility source defaults * Fix host-scoped visibility settings races * Fix global worktree visibility integration * Enable source visibility defaults on mobile * Polish external worktree settings navigation * Clarify inherited worktree visibility settings * feat(sidebar): replace the inherited-visibility switch with a Show/Hide picker Each source row now shows a two-segment Show / Hide control preselected to the global setting, and explains itself only where the project actually disagrees: an "Overriding global setting: <value>" card names the value being ignored. Picking the segment global already holds drops the override instead of pinning a duplicate, so the same control both overrides and reverts, retiring the separate "Use global" link. The dialog footer now lists every inheritable source with its global value. * fix(sidebar): preserve reset for matching visibility overrides |
||
|
|
266b5ae8f5 |
fix(mobile): match desktop project and run target picker (#14457)
* fix(mobile): disambiguate repository locations * fix(mobile): preserve explicit repository ownership * test(mobile): use explicit renderer type * refactor(mobile): match desktop project targets |
||
|
|
2100fb2553 |
fix(runtime): cap remote git.diff and file previews at the transport budget (#14160)
* fix(runtime): cap remote git.diff and file previews at the transport budget A remote or mobile user who opens the diff of a large image loses their whole WebSocket, not just that request: the E2EE channel closes with 1013 when a reply exceeds the 4 MiB outbound envelope. Two producers can exceed it unaided. git.diff/branchDiff/commitDiff cap text with MAX_RENDERED_DIFF_COMBINED_CHARACTERS (6M chars) -- a *renderer* budget that sits above the transport limit -- and return base64 for previewable binaries bounded only by MAX_GIT_SHOW_BYTES, so a 10 MiB PNG changed in place is ~26.7 MiB in one envelope. files.readPreview inlines base64 up to 10 MiB, and mobile calls it for every image tab. Both now measure against a budget derived from the outbound limit. The check sits in orca-runtime-git.ts, downstream of the dedupe and of both the SSH-provider and local branches, so a payload forwarded verbatim by an old relay is covered by the same code and src/relay needs no change. Local and in-process callers pass no budget and keep full fidelity. Measuring raw bytes would not work, which is the whole reason this needs a module. JSON escaping turns one control byte into six (\u00XX), and binary-buffer.ts sniffs only for NUL in the first 8 KiB -- so a NUL-free file of 0x01-0x1f bytes is classified as *text*, would pass a raw-byte cap, and would then blow the envelope. The budget is escape-aware, with a three-branch fast path that keeps normal diffs at two native byteLength calls and scans only the ambiguous band. The SSH branch of readFileExplorerPreview had the same raw-vs-escaped gap: its stat gate sizes base64 binaries, but text crossed unbounded. It now honours the same decoded-text limit the local branch already enforced. No wire change: GitDiffResult is untouched -- no third kind, no new field. Old clients see an error for one request instead of a dropped connection. diff_too_large joins the structured passthrough codes and lands on an existing error arm in both mobile consumers and the desktop remote path; file_too_large was already handled on both. Instruments the 1013 close, which nothing measured before, so the incidence this cap is meant to drive to zero is finally observable. `emitter` separates a producer size bug from a wedged link. Known regression: remote image previews between ~3.096 and ~3.146 MB now return file_too_large. They only intermittently worked before -- above ~3.0 MB they killed the socket -- so this trades intermittent connection loss for a consistent error. Test: 10281 passed in src/main/runtime + src/shared + src/main/git; mobile 3427 passed. Each of the six budget-enforcement sites is independently mutation-killed. Escaping fixtures cover newline-dense, control-char, CJK, lone-surrogate and base64 content against native JSON.stringify. tsc clean for node, web and cli; oxlint clean. Co-authored-by: Orca <help@stably.ai> * fix(runtime): harden remote reply transport budgets * test(runtime): cover desktop remote preview budgets * test(runtime): close telemetry review gaps * chore(shared): repoint budget imports after the shared/types barrel removal Upstream #14447 dropped the shared/types barrel; GitDiffResult now lives in git-diff-compare-types and GlobalSettings in global-settings-types. Co-authored-by: Orca <help@stably.ai> * fix(ssh): surface an over-cap preview read as file_too_large The stream reader aborts an over-cap read with StreamProtocolError, whose numeric code falls through mapRuntimeError to a generic runtime_error carrying the raw "Reported totalSize N exceeds client cap M" string. Neither preview client recognizes that: runtime-file-client.ts and mobile-file-preview-response.ts both key on file_too_large. It also made the two file_too_large guards directly below the read unreachable on the streaming path. Gives the cap its own error type so the caller can translate it, keeping the bandwidth saving the cap exists for. A genuine protocol fault still propagates unmasked. Found by the readiness review. Mutation-verified: removing the translation fails exactly the new test. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai> |
||
|
|
77f23b013f |
refactor(shared): drop the shared/types barrel and import from the real modules (#14447)
#14397 split `shared/types.ts` into 46 per-domain modules but kept the path as a re-export barrel so the import sites did not have to change. This removes the barrel: every consumer now imports from the module that actually declares the type, and `src/shared/types.ts` is deleted. Barrels hide where a type lives, make every consumer look like it depends on the whole domain, and let an unrelated edit invalidate a module that ~2,000 files transitively import. 2,323 import declarations across 2,321 files. Rewritten mechanically: each specifier was resolved to an absolute path via the TypeScript AST and recomputed, rather than string-substituted, so alias forms (`@/../../shared/ types`) and per-specifier `type` modifiers survive. Four cases the mechanical pass had to handle, each found by a gate rather than by reading the diff: - Modules inside `src/shared` import the barrel as `./types`, not `shared/types`. A pre-filter on the latter string skipped 176 of them and left imports dangling at a deleted file, which surfaced as confusing `Property 'x' is optional in type 'Repo' but required in Pick<Repo, ...>` errors rather than "module not found". - The barrel RENAMED one type on the way through (`WorkspaceSource as WorkspaceCreateTelemetrySource`), so the original name in the owning module has to be re-aliased at each consumer. - Three test files put `;(globalThis as ...)` on the line after the import. TypeScript parses that `;` as the import statement's terminator, so replacing through `statement.getEnd()` deletes it and breaks ASI. The rewrite now stops at the module specifier. - A file that already imported directly from a module got a SECOND import from it, because the barrel re-exported those same names — which trips `import/no-duplicates` under `--deny-warnings`. A post-pass merges declarations sharing a specifier and type-only-ness; the `import type` plus `import` pair from one module is left alone, since that form is allowed. Splitting one barrel import into several genuinely adds lines, which pushed `terminal-layout-pty-ownership.ts` to 301 counted lines: its 107-character import must wrap, and neither local type collapses onto one line (101 and 116 characters). Rather than contort a type declaration to fit a line budget, `collectLeafIds` and `pruneLeaves` move to `terminal-pane-layout-tree.ts` — they are pure structural operations on the layout tree and independent of PTY ownership. `visible-worktrees.ts` similarly loses its own mini-barrel re-export of `isDefaultBranchWorkspace`, with the four real consumers repointed at the declaring module. No `max-lines` bypass added. Verified: cold `tsc --noEmit` green on node, cli, and web (buildinfo deleted first — these projects are `composite: true` and reuse stale caches); the full `pnpm lint` green, not just bare oxlint — the narrower local check is what let the duplicate imports reach CI; max-lines ratchet OK at 344. |
||
|
|
583ab1601b |
refactor(shared): group worktree, github, and linear modules into folders (#14437)
`src/shared` is a flat directory of ~1,150 entries. The worktree, github, and
linear domains accounted for 71 of them, so finding the module you wanted meant
scanning a wall of same-prefixed filenames.
Move each domain into its own folder and drop the now-redundant prefix:
src/shared/github-pr-types.ts -> src/shared/github/pull-request-types.ts
src/shared/worktree-id.ts -> src/shared/worktree/id.ts
src/shared/linear-links.ts -> src/shared/linear/links.ts
This follows the existing `network/` and `new-workspace/` convention in the
same directory, which also drop the prefix inside the folder.
Whole clusters move, including tests. Foldering only part of a domain would be
worse than flat: a reader would have to check both `github/` and the flat
directory, and `github-auth-types.ts` / `github-project-types.ts` are type
modules that belong with the rest. No files with these prefixes remain flat.
Import specifiers were rewritten by resolving each one to an absolute path and
recomputing it, not by string substitution, so the `@/../../shared/...` alias
forms are handled correctly. 501 specifiers across 298 files.
Two things `tsc` cannot catch, handled explicitly:
- `github-project-types.ts` carries its own `max-lines` bypass, so its baseline
entry is REPOINTED to the new path rather than pruned. Pruning would drop the
bypass and then flag the new path as a fresh violation. Ratchet stays at 345.
- `mobile/` is outside `pnpm typecheck` and cannot be typechecked here
(`mobile/node_modules` is empty). Instead every relative specifier in the repo
was resolved against the filesystem: 174 unresolved before this change and 174
after — identical, so nothing broke in mobile either.
The pinned `tests/e2e/.cross-version-checkouts` fixtures are deliberately NOT
rewritten; they are a snapshot of an older release and still reference the old
paths.
Verified: cold `tsc --noEmit` green on node, cli, and web (buildinfo deleted
first — these projects are `composite: true` and reuse stale caches).
|
||
|
|
54aa22b2df | fix(mobile): self-heal host opens and harden session liveness (#14333) | ||
|
|
d2850b9296 |
Fix mobile HTML report rendering (#14196)
* fix(mobile): render local HTML reports at phone width * fix(mobile): satisfy browser URL lint |
||
|
|
4882eeb8ac |
rm git shim: neutralize stale wrappers without a host gate (#14255)
* Revert "fix terminal attribution shim removal edge cases (#14187)"
This reverts
|
||
|
|
0f51d0b3bb |
Fix native chat image marker position handling (#14162)
* fix(native-chat): handle image markers in any position * fix(native-chat): preserve image caption whitespace * test(native-chat): cover marker boundary spacing * fix(mobile): normalize image echo reconciliation * fix(mobile): use idiomatic tail access * refactor(native-chat): share image echo matching * perf(native-chat): avoid unchanged block copies |
||
|
|
0ed6db77cf |
fix(mobile): open agent-cited external chat files (#14166)
* fix(mobile): open agent-cited external chat files * fix(mobile): keep cited external files read-only * refactor(mobile): derive cited-file mode from provenance * fix(mobile): accept sentence-final cited paths * fix(mobile): preserve cited SSH grant scope * refactor(file-links): share location suffix parsing |
||
|
|
6b84e33251 |
fix(mobile): trade a lease-only stream for output when leaving a chat tab (#14179)
* fix(mobile): trade a lease-only stream for output when leaving a chat tab Tapping a terminal tab from a native-chat tab left the terminal blank. The route subscribes the incoming handle synchronously in switchTab, while the coverage it reads still describes the chat tab being left, so the handle gets a `mobileInputLeaseOnly` subscribe — the host answers `subscribed` and nothing else, no scrollback and no data frames. Input kept working because it rides a separate terminal.send RPC. The reconciler then cleared its covered marker (the active handle changed), so the stream was active and uncovered — which its state machine could not tell apart from a healthy one, because `streamActive` conflated the two. It settled on 'none' and nothing else repaired it: the route's web-ready path bails on any live subscription. The tab stayed blank until app restart. Track which handles hold a lease-only subscribe and thread it into the reconciler as `streamIsLeaseOnly`, so an uncovered handle holding one resumes into a full stream. The covered branch is untouched, so the input lease that keeps the chat composer from locking forever (#10681) still survives. * fix(mobile): clarify stream reconciliation ownership * fix(mobile): keep stream reconciliation checks clean |
||
|
|
585dd6d3a9 |
fix terminal attribution shim removal edge cases (#14187)
* fix(terminal): fully retire attribution shim * fix(terminal): harden shim tombstone path lookup |
||
|
|
1136503c6a | fix(mobile): use 'unsupported' for the no-review-creation test premise (#14091) | ||
|
|
991a3fe963 |
chore(lint): update oxlint to 1.77 and enable no-op cleanup rules (#13901)
Enable eleven oxlint rules that simplify code without changing behavior, and fix
every existing violation. Each candidate was gated on measured cost rather than
assumption, so rules that regressed runtime performance or type checking were
dropped instead of suppressed.
typescript/no-redundant-type-constituents is the largest addition: 113 sites, no
autofix. Dead constituents are deleted. Where the redundant literal existed to
document intent (`string | 'all'`), it is preserved as `(string & {})`, which
keeps the autocomplete hint the original code was reaching for instead of
flattening it away. The rule also caught a broken import —
remote-shared-control-retirement-probe.ts pulled RuntimeStatus from
src/shared/types, which does not export it, so the type silently degraded to
`any`; no tsconfig covers that file, so tsc never saw it.
oxlint stays at 1.77.0 rather than 1.78.0 because .npmrc sets
minimum-release-age=4320 and 1.78.0 is younger than that window.
Rules evaluated and rejected, with what disqualified each:
- prefer-string-raw: String.raw is a runtime call, not a literal (184x slower)
- prefer-string-replace-all: 26% slower
- text-encoding-identifier-case: ~5% slower, reproducible
- prefer-spread: [...str] is 110% slower than split('') and differs on surrogates
- no-implicit-coercion: `!!x` narrows types and `Boolean(x)` does not (22 tsc errors)
- prefer-arrow-callback: arrows are not constructible, breaking `new` on mocks
- object-shorthand: rewrites source text asserted by a tracked reliability gate
- switch-case-braces: pushes ten files past max-lines, which cannot be suppressed
- no-useless-switch-case: drops `case undefined:` that switch-exhaustiveness-check needs
- arrow-body-style: 115 violations have no fix, and it breaks max-lines
- newline-after-import: false-positives on the leading-semicolon ASI idiom
electron-vite-output-contract asserted on the literal
Object.prototype.hasOwnProperty.call text; retarget it to Object.hasOwn, which
rejects inherited keys identically.
|
||
|
|
2b1e69d4b0 |
Add keyboard dismissal for mobile markdown editor (#13856)
* fix(mobile-markdown): enable keyboard dismissal while editing Allow users to dismiss the soft keyboard while composing markdown content. Extract the MarkdownReader component into its own file and add WebView-based caret preservation to restore the cursor position after the keyboard closes. This prevents the editor from losing focus and erasing the user's selected caret location when the keyboard hides. * improve test |