[WIP] ivrix-prompt-selection-edit-app - #7
Draft
DananzMolt wants to merge 1978 commits into
Draft
Conversation
…all teardown (manaflow-ai#8858) * Add failing regression tests for unbounded mobile event emission (manaflow-ai#8842) A stalled, never-draining terminal.render_grid subscriber must keep the host's pending event queue bounded WITHOUT tearing the connection down: close-on-overflow churns connection resources (sockets, lanes, tasks) every few seconds for as long as the subscriber stays slow, which is the reconnect-churn half of the manaflow-ai#8842 field incident (785 -> 3,200 fds, 4.12 GB RSS, jetsam largestProcess, forced hard reset). Red on this commit: - testStalledRenderGridSubscriberStaysOpenWithBoundedEventQueue: the connection currently closes at bounded capacity instead of shedding recoverable render-grid frames. Green guards committed alongside (documenting contracts the fix must preserve): - testStalledSubscriberOverflowOnNonRecoverableTopicClosesConnection: non-recoverable topics (mobile.sync.delta) keep close-on-overflow. - testCloseReleasesConnectionAndTransportResources: every per-connection resource releases after close, even with a send stalled mid-flight. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Bound mobile event emission with synchronous admission and shed-or-close (manaflow-ai#8842) Root cause (owner boundary): MobileHostService.emitEvent spawned one unstructured Task per registered connection per event, each retaining the full [String: Any] payload, and each connection actor re-serialized the same payload before running its bounded-queue admission check. The bound therefore applied only to encoded frames; everything upstream of it — task allocations, retained payload graphs, the actor mailbox — was unbounded whenever a subscriber drained slower than the producers. Render-grid and terminal-bytes events are continuous (every Ghostty tick x every surface, ~130 surfaces in the field incident), so a slow, paused, or half-dead phone grew host memory and CPU without bound (4.12 GB RSS, 69,946 s CPU, jetsam largestProcess, forced hard reset). Overflow policy was connection teardown, so a slow-but-alive subscriber cycled connect -> fill -> close -> reconnect -> full replay, churning NWConnection/Iroh lane resources (785 -> 3,200 fds). Invariant now enforced: memory, tasks, and connection resources attributable to mobile event emission are O(bounded queue capacity) per connection regardless of subscriber behavior, and every per-connection resource is released deterministically when a subscriber stops draining. Mechanism: - Encode once, admit synchronously: emitEvent encodes the envelope a single time and admits it into each connection's new MobileHostConnectionEventQueue (lock-protected, count- and byte-bounded) on the emitter's thread. No per-event tasks; at most one drain task per connection, claimed through the queue. - Shed instead of close for recoverable topics: render-grid frames are shed per surface under overflow; the surface is poisoned against further deltas (the iOS client has no delta-continuity check, so a silently dropped delta would corrupt its grid invisibly) until the producer — asked via MobileTerminalRenderObserver's coalesced full-resync hook — re-emits a full frame that re-bases every subscriber's chain. terminal.bytes (client detects seq gaps and replays) and terminal.updated/workspace.updated (level-triggered pings) shed without extra recovery. Non-recoverable topics keep the close-on-overflow contract. - Deterministic teardown for half-dead peers: control-lane event writes now run under a bounded stall deadline (default 30 s, injectable) — a peer that accepted the connection but stopped reading previously pinned the drain, queue, transport, and tasks forever. The Iroh independent event lane already had its own 3 s deadline. - Render-grid CPU: the frame is JSON-encoded once and spliced into the event envelope, replacing the encode -> parse -> re-serialize round trip that showed up in the incident's cpu_resource stacks. Turns the commit-1 regression test green and adds fan-out, stall- deadline, and queue-admission policy coverage. Fixes manaflow-ai#8842. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Drop new ForTesting seam; read the internal event queue directly in tests Review feedback: the queue is an internal nonisolated let, so the test reads eventQueue.count/byteCount via @testable import instead of a new debug wrapper. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: cmux reload-cloud <cmux-reload-cloud@users.noreply.github.com> Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…' into issue-8808-typing-lag-regression
…anaflow-ai#8221) * Mac side: mobile.workspace.changes.* RPCs backed by CmuxGit WorkspaceChangesService Adds a subprocess-backed workspace-changes service (summary/files/file_diff vs merge-base of the default branch, untracked included, 15s summary TTL cache, path containment validation, 400KiB/6000-line hunk-aligned diff truncation) and exposes it to the phone as three mobile data-plane RPCs with capability workspace.changes.v1. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS data layer: changes DTOs, CmuxMobileChanges parser package, composite integration Lenient DTOs for the three mobile.workspace.changes RPCs, a Foundation-only CmuxMobileChanges package (unified-diff parser with line numbering and CRLF preservation, grapheme-safe intra-line emphasis, clamped diff font preference), and MobileShellComposite integration: workspace.changes.v1 capability gate, 64-id batched summary fetches with a 15s reuse window, rpcWorkspaceID-keyed chip snapshots, and a cancellable 250ms debounce off list refreshes and workspace.updated events. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS UI: Changes sheet — file list, swipe-paged diff viewer, preview route Value-driven Changes screens in CmuxMobileChanges (GitHub-calibrated adaptive theme, summary header, status-glyph file rows with mini add/delete bars, dual-gutter soft-wrapped unified diff with intra-line emphasis, page TabView with position pill, pinch font sizing, copy line/hunk), the ShellUI sheet mount with parsed-document cache, and the deterministic CMUX_UITEST_CHANGES_PREVIEW fixture route (populated/diff/empty/states). All strings localized en+ja. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * iOS entry points: Changes toolbar button, workspace-list chips, one-time hint One shared openWorkspaceChanges() action presents the Changes sheet from the capability/connection-gated toolbar button (badge capped at 99+) and the dismissible first-time hint banner; workspace rows get an ambient +A −D chip fed by value snapshots keyed by rpcWorkspaceID. Also renders 'No newline at end of file' markers as dimmed gutter-less rows (parser emits them; Copy Hunk excludes them). All new strings localized en+ja. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Keep wire DTO types out of ShellUI: map change status in the shell layer CmuxMobileShellUI never imports CmuxMobileRPC; the status→FileChangeKind mapping moves into CmuxMobileShell (which gains a CmuxMobileChanges dependency) so the sheet consumes model values by member access only. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Render an honest not-a-repository state in the Changes sheet A workspace whose directory is outside any Git repository previously fell into the generic connection-error state. The composite now maps the not_a_repo RPC code onto a shell-owned WorkspaceChangesFetchError and the list renders a dedicated localized state (folder.badge.questionmark, no summary header, no retry). Verified live against the tagged Mac's home workspace. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix macOS Debug build broken on main: explicit color capture in NSImage draw closure Sources/Sidebar/AppKitList/Cells/SidebarWorkspaceRowSlotViews.swift from manaflow-ai#8034 references the slot view's color property inside the escaping NSImage draw handler without explicit capture, which fails to compile (CI is currently advisory, so it landed unnoticed). Capturing the color value keeps the view out of the image's retained draw block. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Render the changes chip in the shared WorkspaceRow so the UIKit list shows it Main's UITableView workspace list (manaflow-ai#8186) hosts WorkspaceRow directly, bypassing WorkspaceNavigationRow where the +adds −dels chip lived, so chips vanished after merging main. The chip (and its localized accessibility label) moves into WorkspaceRow itself, both pipelines pass it through, and the table coordinator reconfigures exactly the cells whose chip value changed. Also keeps concise changes.summary debug-log lines that made this diagnosable from the container log. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Calm the file list: text badges instead of a status-icon zoo Five leading pictograms in four hues made the list read as a barrage of symbols. The row now leads with the path; magnitude stays on the counts and mini-bar; and only exceptional states get a quiet capsule badge in the BIN badge's language: green 'New' (added and untracked collapse into one concept), red 'Deleted' with the whole path dimmed. Renames keep only their old → new line. Modified rows, the common case, carry no marker at all, and the palette drops to green/red (orange and blue status tokens removed). Badges are localized en+ja; VoiceOver labels keep the full status wording. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Say what the app means: 'Binary' badge and a truncation footer that explains itself 'BIN' was insider shorthand; the badge now reads Binary (ja already said バイナリ). The truncation footer stops announcing a mechanism and states the tradeoff: 'Large diff. Showing the first N lines to keep things fast. See the rest on your Mac.' The 6,000-line/400KiB per-file cap itself is unchanged; it exists so generated files and lockfiles cannot balloon the RPC payload or phone memory. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * View changed binary files with the artifact viewer, at either revision Changed images, PDFs, and other binaries stop dead-ending at a placeholder: the diff page's binary card offers View Before / View After (or a single View File for added, untracked, and deleted files) and pushes the shared ChatArtifactViewerDestination — zoomable images, PDFKit, AVKit, QuickLook — fed by two new data-plane RPCs, mobile.workspace.changes.file_stat and .file_fetch. Reads are authorized against the workspace's current changed-file set (rename old paths only for revision=base) plus the path containment check; base blobs materialize once via git show into an actor-owned 256 MiB LRU temp cache and serve 3 MiB chunks with honest EOF math. Loader cache scope keys by workspace + revision + path so before and after never collide. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Log workspace-changes content failures to the container debug log One line per failed stat/fetch with method, params, and the underlying error, matching the changes.summary logging style, so preview failures are diagnosable from the device log instead of a generic viewer state. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Binary previews render inline with their actions in place Paging onto a changed image or PDF now shows the content immediately: the page hosts a chrome-free ChatArtifactInlineViewer (new public component reusing the pager's per-type hosts and lifecycle), with a Before | After selector for modified and renamed files. The full-screen hop is gone; the viewer toolbar's Share / Save to Files / Copy-image actions render in place, conditional on the loaded content type, through a factored ChatArtifactActionBar the full viewer now shares. Zoom coexists with page swipes the way Photos does: a zoomed-out pan falls through to the pager. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Preview actions live in the sheet toolbar, conditionally for the current page Share / Save to Files / Copy image move from floating pills into the Changes sheet's top-trailing navigation toolbar, driven by an Equatable descriptor the inline viewer publishes via a SwiftUI preference (execution stays in the viewer through a registration-generation host, so a stale page can't clear a fresh performer). Only the selected pager page mounts a preview, so the toolbar always reflects the visible file and empties out on text pages. Includes the fix that attaches the toolbar group to the pushed pager screen, which owns its own navigation bar, instead of the sheet root. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Copy image via standard glyph and image long-press menu The copy-image action now uses the doc.on.doc copy symbol instead of the gallery-reading photo-on-rectangle glyph, everywhere the shared ChatArtifactAction metadata is consumed (Changes sheet toolbar and full viewer). Long-pressing a rendered image presents Share, Save to Files, and Copy image through a UIContextMenuInteraction on the hosted UIImageView, routed through the same performers as the toolbar; the full viewer's copy-image performer now actually copies the rendered image. Adds a changes.hint debug-log line reporting hint eligibility inputs. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Add Changes row to the workspace title menu A second, non-toolbar entry point: the workspace title menu now has a Changes row (shown whenever the host supports workspace changes) routing through the same openWorkspaceChanges() action as the toolbar button. The toolbar keeps the existing +/- icon button unchanged. Also factors the list chip's +N -M text into a unit-tested WorkspaceChangesChipTextPolicy that falls back to a localized file count for binary-only change sets. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Tappable list chip, counts in the Changes toolbar button, drop menu row Three entry-point changes from the placement interview. The +N -M chip on workspace-list rows is now a button that opens that workspace's Changes sheet directly over the list (both the SwiftUI List and UIKit-table pipelines; row selection untouched). The workspace-detail toolbar button replaces its abstract +/- glyph with the same green/red counts whenever the tree is dirty, falling back to the glyph when clean; counts resolve their colors against the terminal theme's chrome scheme rather than the system scheme so they stay legible on dark chrome. The title-menu Changes row is removed as redundant. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Stack the toolbar Changes counts vertically The workspace-detail toolbar button now stacks +N over -M so the counts cost no more horizontal space than a plain icon button. The shared chip label gains a stacksVertically variant used only by the toolbar; list rows keep the horizontal layout. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Failing test: oversized single hunk truncates to nothing A file whose first diff hunk alone exceeds the 6,000-line/400KiB cap comes back as a header-only diff, which the phone renders as "Showing the first 0 lines" with an empty page. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Split oversized first hunk instead of emitting an empty diff When the first hunk alone exceeds the byte/line cap, emit as much of its body as fits under a hunk header rewritten to describe the partial body (start lines preserved, old/new counts recomputed), so the phone shows the head of the change instead of "the first 0 lines". Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Make diff laziness per-line so huge hunks scroll The diff body lazily rendered per HUNK, so a single multi-thousand-line hunk (e.g. a truncated 6,000-line rewrite) became one eagerly laid-out child: seconds of layout and frozen/stuttering scrolling. DiffRowSnapshot now flattens hunks into per-line rows and the LazyVStack iterates those, so only visible lines lay out regardless of hunk shape. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Expand hidden unchanged lines GitHub-style in the diff reader Hidden unchanged regions (above the first hunk, between hunks, after the last hunk) now show tappable expander bands: 100-line steps, full reveal when 120 or fewer remain, split up/down bands between hunks. Revealed lines come from the current working-tree file over the existing authorized chunked file_fetch path (fetched once per file, 5 MiB cap, inline retry on failure) and render as per-line context rows with both gutters mapped through the hunk offsets, preserving per-line laziness. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Progressive Show more past the diff cap and unified short-gap expanders The 6,000-line/400KiB per-file cap becomes progressive: file_diff accepts an optional max_lines (clamped 6,000...1,000,000 lines / 64 MiB abuse guard, byte budget scaled proportionally) and reports diff_total_lines, and the truncated footer becomes "Showing X of Y diff lines" with a Show more button that requests 4x the current budget and replaces the document in place (stable row IDs preserve scroll and expansion state). Expander bands whose whole run reveals in one tap now render a single unified button instead of a split up/down pair whose halves did the same thing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Address review findings: bounded work, truthful truncation, stable routes Fixes the six accepted P1 review findings plus bot feedback: - Clamp progressive diff responses to 6 MiB so they always fit the 8 MiB RPC frame; the client stops offering Show more when a larger budget stops growing the loaded window. - Suppress the trailing context expander on truncated diffs (the region after the last included hunk is not known unchanged). - Read git diff output through a bounded incremental reader (terminate past the budget) and report the diff total as unknown when cut short. - Size base blobs with cat-file before materializing, stream git show to the temp cache incrementally, refuse blobs over the cache budget, and never pin an oversized entry through eviction. - Parse diff responses off the main actor and cache the flat row projection in state instead of rebuilding it per body evaluation. - Apply the 500-file cap before untracked-file inspection and count untracked additions with bounded in-process reads instead of one git process per file. - Decode summary identity fields strictly (lossy batch drops malformed entries), route the diff pager by stable file path with a fail-closed missing state, single-pass prefix truncation, read summary-cache entries after the suspension point, and scrub RPC parameter names from user-facing error copy. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Bound snapshot and inspection work, fail closed on remote provenance Second review round: snapshot git commands stream through the bounded reader with a 32 MiB ceiling, 30s wall deadline, cancellation checks, and explicit truncation; untracked inspection gets a 64 MiB aggregate budget with cancellation between files; the client clamps Show more progression at 96,000 lines and builds the parsed document, row projection, and gutter width together off the main actor as one immutable presentation; intra-line emphasis is skipped for lines over 4,096 UTF-8 bytes before any Character materialization; remote-provenance workspace paths never reach local git (summary reports not a repository, content verbs return not_a_repo); both process-lifetime caches purge expired entries globally and hold at most 64 entries with LRU eviction. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Move directory policy into CmuxGit and bound zero-byte cache entries The workspace-directory provenance policy is consumed by the macOS app target, so it lives in CmuxGit's changes domain (CmuxMobileRPC is an iOS-group package the Mac app cannot resolve; its tests move to CmuxGitTests). The base-content cache adds a 256-entry LRU count bound so zero-byte blobs, which are invisible to the byte budget, cannot grow the entry map and temp-file population without limit. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Scope refresh fanout, decouple pinch from rows, bound caches, pin revisions Review round 4: workspace deltas schedule changes-summary refreshes only for the delta's workspace IDs (group-only deltas skip entirely); pinch no longer rebuilds the row projection (rows depend only on document, expansion, and current lines; gutter width derives cheaply at render); the sheet's parsed-presentation cache is a 7-entry LRU around the selected page; file_diff/file_stat/file_fetch carry an additive stat fingerprint so expansion fetches from a newer working tree are discarded and the diff refreshed instead of splicing mixed revisions; Show more now continues when the host cannot report a total, with loaded-only progress copy (en+ja). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Harden content reads, cache keys, fingerprints, and refresh coalescing Review round 5: expansion downloads enforce a cumulative 5 MiB cap and chunk-count ceiling inside the transport loop; the diff's content fingerprint stats the working file before and after git runs and returns a never-matching unstable token when they differ; base blobs are keyed and fetched by an immutable commit OID instead of the moving HEAD ref; content reads walk the validated path component-by-component with O_NOFOLLOW anchored at a repository-root descriptor so a post-validation symlink swap cannot escape the repository; the summary-refresh debounce accumulates a union of pending workspace IDs with a dominating all-workspaces flag instead of dropping scopes on restart. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Recoverable cancellation, pinned transfers, safe inspection, guarded publishes Review round 6: connection-transition CancellationErrors publish the error state (silent stop only under real task cancellation); chunked content transfers resolve scope, authorization, base OID, and base size once and stay pinned via the authorized-path cache, which is now revision-keyed so a moved base refreshes the snapshot; artifact transfers verify every chunk's content fingerprint against the initial stat; untracked inspection reuses the O_NOFOLLOW component-walk opener and rejects symlinks and non-regular files with cancellation checks; diff load, Show more, and expansion publishes are generation-guarded so a superseded request can never overwrite a newer presentation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Single-flight summaries, bounded parsing, post-read fingerprint checks Review round 7: the summary debounce is separate from the fetch, which is single-flight with a trailing coalesced pass instead of being cancelled by every workspace delta; expansion line materialization runs on a nonisolated worker with a 200,000-line bound; snapshot output parses incrementally keeping at most the 500-entry cap plus running totals instead of materializing unbounded path collections; content chunks fstat the descriptor again after reading and fail closed when identity, size, mtime, or ctime moved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Literal pathspecs, fail-closed fingerprints, serialized expansion Review round 8: Git commands taking network-selected paths run with literal pathspec semantics; a working file that changes across the diff capture retries once then fails with the retryable error instead of publishing content with an unstable token; once a fingerprint is established every subsequent response must carry a matching token (nil observed fails closed, all-legacy hosts keep working); cached-lines expansion sets pending state and coalesces reveal intents into one cancellable rebuild; skipped-fresh summary refreshes arm one trailing fetch at expiry and an additive force param bypasses the host's TTL cache; the full image viewer regains Copy Path; the UIKit workspace table's height caching accounts for chip presence so interactive chips never clip. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Deadline every git call, typed repo outcomes, leases, identity fingerprints Review round 9 accepted findings: the plain runner overload gains the same 30s deadline and cancellation termination as the bounded overloads; unborn repositories diff against the empty tree so untracked files list (git failures map to gitFailure, never notARepository); truncated changed-file snapshots render a bounded-result footer; untracked files past the scan budget are prefix-probed and classified binary when unknown; chunked base transfers hold eviction leases on their cache entries; fingerprints carry device, inode, and ctime so same-size same-mtime replacement is detectable. Two round-9 findings rejected by design and documented in place: the default-branch HEAD comparison fallback, and delta+TTL-driven summary refresh (repo-watching is a follow-up). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Blob-identity fingerprints, hard git deadlines, budget-honest leases Review round 10: base-revision fingerprints derive from the immutable commit and blob OIDs so cache eviction cannot change a transfer's identity; the git deadline terminates the process group, unblocks pipe readers, and escalates to SIGKILL after a grace period; the base cache reserves projected bytes before materializing, rejects when no unleased victim can satisfy the budget, and leases every returned URL through its use; missing fingerprints fail closed (this protocol always emits them); Show more tasks are retained and cancelled with generation invalidation when a page disappears; read-capped untracked counts are marked partial and flip the snapshot's truncated flag. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Scope legacy refreshes, pin diff base to OID, prompt exits, self-expiry Review round 11: legacy workspace.updated reloads schedule TTL-respecting summary refreshes instead of forced app-wide sweeps; the verified base commit OID is the diff base for every operation (symbolic name kept only for display); the git deadline path stops waiting out the SIGKILL grace once the process group is gone; successful summary fetches arm the trailing expiry so chips self-refresh after the TTL. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Group-liveness deadlines, honest TTLs, pruned state, bounded page memory Review round 12: git deadline termination and escalation track process-group liveness so descendants holding stdout are reaped; summary TTLs stamp at batch completion with a floored trailing delay so slow hosts cannot loop at zero delay; summary state prunes against the current workspace set and consumes state-sync removals before rearming; diff pages hold heavy state only in a selected-neighborhood window, with other tabs mounting on selection; the workspace table's height cache keys by digit-count buckets and chip mode instead of exact live totals. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Restore submodule pointers, cache snapshots, gate polling, unpin executors Review round 14 (Claude engine): the origin/main merge had resolved the ghostty and vendor/bonsplit gitlinks to older branch-side commits; both are restored to main's pointers. File diffs and changed-file lists now serve a 15-second LRU loaded-snapshot cache so pager mounts reuse one repository walk (force bypasses it); the summary trailing refresh only re-arms while workspace events are recent, so an idle connected phone cannot hold the Mac in a perpetual 15-second git poll; blocking git spawn/poll/reap loops run on a dedicated GCD queue bridged with continuations instead of pinning the cooperative executor; the developer-scratch live-repo probe test is removed; SwiftUI list rows gate the chip tap closure like the UIKit path so chip-less rows keep combined VoiceOver navigation. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Move base-revision git and large decodes off blocking executors Review round 15: the base-content cache takes an async materializer so the actor suspends instead of blocking on git show (post-await collision adopts the winning entry); the rev-parse and cat-file probes and the materializer run through the dedicated blocking queue per the service's own executor contract; multi-megabyte file-diff and content-chunk JSON payloads decode in nonisolated async helpers so Show more and binary previews never run their decode pass on the main thread. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Live cancellation on GCD git work, bounded hint keys, injected clock Review round 16 nits: a thread-bound cancellation signal bridges Swift task cancellation into the GCD-hosted git loops (Task.isCancelled reads false there), so unmounted pages and dropped connections stop subprocess reads and untracked scans early instead of riding out the wall deadline; the hint-dismissal store keeps seen workspace IDs in one 256-entry FIFO array key instead of unbounded per-workspace defaults keys; the summary debounce and trailing-expiry sleeps use an injected Clock so scheduling is test-drivable. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Assert renamed-file diffs are paired, not add-only The rename test accepted any non-empty diff, so a full-file addition (the current behavior) passed. It now requires rename headers and no content lines as additions for a pure git mv. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Pair renamed-file diffs by including the old path in the pathspec git applies pathspec filtering before rename detection, so fileDiff's new-path-only pathspec made -M unable to pair renames: the diff page showed the whole file as added while the file list's paired numstat showed the true +/- counts. Tracked diffs now pass both the validated old and new paths after --, restoring similarity headers and an empty hunk body for a pure git mv. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Assert CRLF diffs split, count, and truncate per line Red test: the truncator must count each CRLF-terminated content line and break at hunk boundaries inside CRLF diffs. Character-based splitting treats \r\n as one grapheme, so today the whole CRLF hunk body is one mega-line and these assertions fail. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Split diff lines on literal newlines so CRLF hunks survive truncation Character-based split treats \r\n as one grapheme, so CRLF diff bodies collapsed into a single mega-line: totals undercounted, interior hunk headers went undetected, and an over-cap CRLF diff truncated to metadata-only text that the phone rendered as an empty diff. The truncator now splits with components(separatedBy: "\n"), matching the iOS UnifiedDiffParser's handling of the same pitfall. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Keep content reads and diff truncation off the cooperative pool fileStat's open+fstat, fileFetch's chunk read (up to 3 MiB), and fileDiff's decode+hunk-split of up to ~13 MiB of git output ran on Swift-concurrency cooperative threads; a repo on a network or external volume could pin one for seconds per call. All three now route through the same offCooperativePool seam as the service's git subprocess work. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Retire the trailing expander once a fetched file proves it empty On added files and EOF-touching diffs the trailing "Expand hidden lines" band was a permanently dead control: the tapped gap resolved to nothing against the fetched file, and that path cleared pending state without publishing the fetched lines, so the projection never learned the line count, the band never disappeared, and every tap re-downloaded the whole file. The nil-gap path now recomputes the presentation with the fetched lines, which removes the band and caches the lines. Covered at the projection level (band present without a line count, gone with one); a true page-level red/green is not practical because the page is @State-bound SwiftUI rather than an observable model. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Decode changed-file paths strictly so malformed entries are omitted File.init decoded every field leniently, so an entry missing its path became path "" instead of throwing: the batch loop's omission filter never fired, the list showed a nonsense row whose diff request the host rejects, and two such entries collided on the path-keyed SwiftUI identity. The path now decodes strictly and rejects empty strings, matching the sibling summaries decoder (strict identity, lenient counts), so identity-less objects are dropped like non-object entries. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: cmux reload-cloud <cmux-reload-cloud@users.noreply.github.com>
* Fix main build: import CmuxWorkspaces in DockSplitStore+RestoredAgentLifecycle PanelShellActivityState moved into the CmuxWorkspaces package, and manaflow-ai#8690 merged a file that references it with only a Foundation import, so current main fails to compile the macOS app (cannot find type 'PanelShellActivityState' in scope). Sibling users of the type such as Workspace+AgentLifecycle.swift already import CmuxWorkspaces. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix main build: explicit return for trailing switch expression surfacePromptForResumeApproval ended with a bare switch statement whose cases are contextless member expressions (.auto/.prompt/.manual), which does not compile as a statement. Use 'return switch' so the cases get their contextual type from the return type. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * Fix main build: annotate closure result types in DockSplitStore+SessionSnapshot The Xcode 26.5 toolchain on the reload builder cannot infer result types of multi-statement closures that return nil-or-value, failing with 'generic parameter could not be inferred' at the Dictionary(uniqueKeysWithValues: compactMap) pair builder and the observation.flatMap guard. Annotate both closures with their concrete result types. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…anaflow-ai#8868) The feed row's swipe action was trailing and only offered Mark as Read on unread rows. Move it to the leading edge and make it a read/unread toggle, matching the workspace list rows: unread rows get Mark as Read (envelope.open), read rows get Mark as Unread (envelope.badge). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
…Lifecycle (manaflow-ai#8873) PanelShellActivityState lives in CmuxWorkspaces; the new extension file from manaflow-ai#8690 only imported Foundation, so every macOS app build from main fails (cannot find type in scope). Sibling files using the same type already import CmuxWorkspaces. Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* test: cover inline attachment render reuse * fix: cache composer attachment thumbnails * fix: bound attachment thumbnail work * fix: cancel deleted attachment thumbnails * test: cover attachment thumbnail cancellation races * fix: preserve thumbnail work across attachment reuse * fix: isolate inline attachment cell rendering * fix: keep thumbnail task teardown actor-safe * fix: support Xcode 16 thumbnail value types * test: isolate attachment deletion undo history * Fix compile errors after main sync * test: cover direct attachment deletion undo * fix: reconcile attachment rendering on undo changes
The inherited section still offered a cmux download badge, so someone skimming the page could install upstream cmux and wonder where the Hebrew support went. Labels that section as upstream and points back to the Ivrix release.
Fixes two reordering bugs on RTL rows: - A neutral that resolved LTR (the hyphen in "max-height", or a plain space between two Latin words) was assigned embedding level 0 instead of 2. Level 0 sits below the paragraph level, so UAX manaflow-ai#9 L2 ended its level-1 run there and reordered the two halves separately: "max-height" rendered as "max <hebrew> -height". - Number separators were never resolved as weak types, so "1.5" rendered as "5.1", "3,000" as "000,3", and "192.168.1.1" came out fully reversed. Claude-Session: https://claude.ai/code/session_01TPRwsvsc1t88Wg5LaJ3rXb
Selecting at a prompt and typing replaces the selection; backspace and delete remove it. Also fixes prompt click-to-move spending two arrow keys on a wide character. Claude-Session: https://claude.ai/code/session_01TPRwsvsc1t88Wg5LaJ3rXb
Selection editing was inert on fish, which advertises click_events rather than cl. It now places the cursor with arrow keys instead of relying on the shell's click support, which fish advertises but does not honor for injected events. Claude-Session: https://claude.ai/code/session_01TPRwsvsc1t88Wg5LaJ3rXb
Tagged dev builds identified themselves as "cmux DEV <tag>", which is the upstream's name, not this fork's. Split the visible label out of APP_NAME into DISPLAY_NAME and set only CFBundleName / CFBundleDisplayName from it. APP_NAME still names the .app directory, because that has to keep matching what xcodebuild emits (via BASE_APP_NAME) and what the rest of the dev tooling greps for: cleanup scripts, the debug CLI, the UI tests and the docs all match on "cmux DEV". Renaming the directory is a separate, wider change. An explicit --name still wins for both, so a build is never called one thing on disk and another in the Dock.
Shift+Left/Right selects by character, Shift+Option+arrow by word, with the direction mapped by row so it reads correctly in Hebrew. Also fixes cell backgrounds, selection highlight and decorations being painted at the logical column instead of the visual one on RTL rows. Claude-Session: https://claude.ai/code/session_01TPRwsvsc1t88Wg5LaJ3rXb
Mouse positions now invert the row's bidi order, so selecting, clicking and link hit-testing land on the cell under the pointer in Hebrew. Claude-Session: https://claude.ai/code/session_01TPRwsvsc1t88Wg5LaJ3rXb
Prompt selection editing, Shift+arrow keyboard selection, and the bidi fixes that make both correct on Hebrew rows. Also gives the app its own version. Ivrix 1.0.0 shipped carrying cmux's 0.64.20, so nothing could compare Ivrix releases to each other. Claude-Session: https://claude.ai/code/session_01TPRwsvsc1t88Wg5LaJ3rXb
The sidebar button runs Sparkle against SUFeedURL, which named upstream cmux.
An Ivrix build asking upstream for updates is offered upstream's releases, and
because they are different applications, accepting one replaces Ivrix with
cmux and loses the Hebrew build. Ivrix 1.1.0 shipped in that state.
Three places had to change, and the second is the one that would have bitten
again later:
- Resources/Info.plist SUFeedURL now names this repo.
- UpdateFeedResolver's default fallback did too, and it is used whenever the
plist value is missing or empty. Fixing only the plist would have left a
silent path back to upstream.
- build-ivrix.sh stamps the feed URL and the EdDSA public key into the built
bundle, and refuses to build if the feed still points upstream or if the
public key is missing or is upstream's. A wrong value here is worse than
having no updater, so it fails the build rather than warning.
Adds scripts/ivrix-appcast.sh to generate and sign the appcast a release needs;
without that asset the feed URL 404s and no update is ever offered. The signing
key is read from the login Keychain by sign_update and never touches the repo.
Still needed before an Ivrix build can actually self-update: generate the
keypair once with Sparkle's generate_keys and rebuild with its public half.
macOS ships bash 3.2, where `set -u` treats an empty array expansion as unbound, so the appcast script aborted before signing.
Reverts the gating. In a right-to-left session, going 'back' with the arrow that points along the reading direction is the wanted behaviour, including inside a full-screen application. The mismatch with an application's own 'press <-' hint is a display problem, not a key-handling one. Claude-Session: https://claude.ai/code/session_01TPRwsvsc1t88Wg5LaJ3rXb
The RTL/LTR control in the titlebar was the only way to flip the terminal print direction. Add a `toggleTextDirection` shortcut action, bound to Ctrl+Cmd+H by default, plus a matching View menu item. All four entrypoints (titlebar control, toolbar segmented control, menu item, shortcut) now flip through one path, `TerminalTextDirectionSettings.toggleDirection()`, instead of each site computing the next direction itself. Ctrl+Cmd+H is free of AppKit reservations and of every cmux default; Cmd+H alone is Hide Application, so the Ctrl variant does not shadow it. Per the shortcut policy the action is in `KeyboardShortcutSettings`, editable in Settings > Keyboard Shortcuts, settable as `shortcuts.bindings.toggleTextDirection` in `~/.config/cmux/cmux.json`, and documented in the shortcut docs and the cmux-settings skill reference. Localized across all 20 supported locales.
Hebrew layouts put HEBREW PUNCTUATION GERESH (U+05F3) and GERSHAYIM (U+05F4) on the apostrophe and quote keys, so `echo "hi"` typed in Hebrew arrives as `echo ״hi״` and the shell never sees a quote. Rewrite those two scalars to ASCII `'` and `"` on the way into the terminal, on both typed-text paths: the `insertText` accumulator and the `textForKeyEvent` fallback. The rewrite is gated on `keyTextAccumulator` being non-nil, so it only applies to live keystrokes — paste, dictation, and programmatic NSTextInputClient callers keep their text verbatim. `normalized` scans for the two scalars before reading UserDefaults, so ordinary typing pays one pass over a one-scalar string and nothing else. On by default, since shell quoting is the common case. Settings > Terminal > ASCII Quotes on Hebrew Layout turns it off for typing Hebrew acronyms such as צה״ל, which need the real gershayim; also settable as `terminal.hebrewAsciiQuotes` in `~/.config/cmux/cmux.json`. Localized across all 20 supported locales. Also adds the direction-toggle coverage for the shared `TerminalTextDirectionSettings.toggleDirection()` path added in the previous commit.
The titlebar RTL/LTR button's hover tooltip named only the current direction, so the new Ctrl+Cmd+H binding was undiscoverable from the control it drives. Route it through `Action.tooltip(_:)`, the same helper the sidebar, notifications, and focus-history buttons already use, so the tooltip reads "Right-to-left (⌃⌘H)" and follows a rebind.
Selection editing at a prompt no longer refuses on the sole grounds that an application owns the screen. It now requires what it actually needs: cells marked as input by OSC 133 `B`. No behavior change today. Claude Code, and every other full-screen application I know of, emits no OSC 133, so their composers are still copy-only. This makes Ivrix ready for the ones that mark, and turns the open question into an upstream ask rather than a terminal-side guess about where somebody else's input box begins.
Keeps the terminal-side half and the upstream request that unblocks it in one place, so the next person to touch selection editing can see why it stops at the composer of a full-screen application.
…tes on a Hebrew layout
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Parked by
open-wip-prs.shon 20260802-234157.29 commit(s) that existed only on this machine. Opened so the work can be
continued elsewhere and the local worktree freed.
Resume:
git fetch origin && git switch ivrix-prompt-selection-edit-app