From c5f78e6e9d25d87879ae30261f3e01ae3c3bb7b2 Mon Sep 17 00:00:00 2001 From: scgopi Date: Tue, 1 Sep 2026 12:10:43 -0700 Subject: [PATCH] Artifactory: `read` sends its request; the rail draws the whole board MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two bugs, one found by a peer loop and one by a human, both real. `graphcode artifactory read ` timed out on every id, valid or not (exit 75), while sync/list/status answered fine from the same daemon. The `openProject` send that opens the round-trip had been glued onto the end of the preceding comment line — syntactically a comment, so it never ran, and `read` then waited for a `.graphChanged` nobody had asked for. Broken since the read-side round of #229; never worked in any release. Diagnosed to the line by the Artifactory demo GIF loop, which is the first time a loop on this graph has found a bug by using the board. A newline fixes it. The rail's expanded ARTIFACTORY section drew its header and the "Leave a note" button with nothing between them, while the folded line showed a post was there. The scroll box asked for `min(contentHeight, cap)` with `contentHeight` starting at zero, and a zero-height scroll view never lays out its content — so the preference that would have grown it never fired. Chicken and egg, resolved by leaving the box unsized until the measurement lands: the first pass lays the posts out, the height arrives, the box snaps to it. Every note is drawn, in a box that hugs them until roughly ten and scrolls after. Suite: 1482 tests / 153 suites pass; swiftlint 0 errors, format clean. `read` is verified live after install; the rail still needs a human's eyes. Signed-off-by: scgopi Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019A6NULwEiBXEdRXwEKKcRH --- graphcode-cli/Sources/main.swift | 3 ++- .../LoopWorkspace/ArtifactorySection.swift | 23 +++++++++++++------ 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/graphcode-cli/Sources/main.swift b/graphcode-cli/Sources/main.swift index 8516b3f9..d66e3619 100644 --- a/graphcode-cli/Sources/main.swift +++ b/graphcode-cli/Sources/main.swift @@ -432,7 +432,8 @@ do { case .artifactoryRead(let projectPath, let postID): // Read-only: the post rides the snapshot, no command is sent, no cursor moves — // the deep-read half of `sync --headlines` triage, priced at one line of context - // per post a loop actually decides to care about. try client.send(.openProject(path: projectPath)) + // per post a loop actually decides to care about. + try client.send(.openProject(path: projectPath)) let read = try client.waitForEvent { if case .graphChanged = $0 { return true } else { return false } } diff --git a/graphcode/Sources/Features/LoopWorkspace/ArtifactorySection.swift b/graphcode/Sources/Features/LoopWorkspace/ArtifactorySection.swift index 38074451..96e3d603 100644 --- a/graphcode/Sources/Features/LoopWorkspace/ArtifactorySection.swift +++ b/graphcode/Sources/Features/LoopWorkspace/ArtifactorySection.swift @@ -63,10 +63,12 @@ struct ArtifactorySection: View { /// question, not a way you prefer to read the board. /// How tall the posts actually are, so the scroll box can ask for exactly that. @State private var contentHeight: CGFloat = 0 - /// Past this the board scrolls rather than pushing the sections under it off the rail. - /// Roughly four posts at the rail's default width — enough that scrolling is the - /// exception, not the way the section is normally read. - static let maxScrollHeight: CGFloat = 300 + /// Past this the board scrolls rather than pushing the sections above it off the rail. + /// Every note is on the board and reachable — this is only how much of it is on screen + /// before the wheel takes over. About ten posts at the rail's default width, which is + /// the number asked for: enough to read a conversation, not so many that the rail + /// is nothing but the board. + static let maxScrollHeight: CGFloat = 600 @State private var showsRecords = false @State private var isComposing = false @@ -116,9 +118,16 @@ struct ArtifactorySection: View { // pinned the posts to the bottom of that box while the header stayed at its top — // so a board with two notes on it drew a header, a stretch of nothing, and then // the notes. Measuring the content and asking for exactly that height (up to a - // cap) leaves no slack for the anchor to spread, which is why the gap cannot come - // back rather than merely being smaller. - .frame(height: min(contentHeight, Self.maxScrollHeight)) + // cap) leaves no slack for the anchor to spread. + // + // `nil` until measured, never zero. This started as `min(contentHeight, cap)` + // with `contentHeight` at 0, and a zero-height scroll view never lays out its + // content — so the preference that would have grown it never fired, and the + // expanded section drew a header, the button, and nothing between. Unsized for + // the first pass, the content is laid out, the measurement lands, and the box + // snaps to it. The failure mode is now a brief greedy box, not an empty one. + .frame(height: contentHeight > 0 ? min(contentHeight, Self.maxScrollHeight) : nil) + .frame(maxHeight: contentHeight > 0 ? nil : Self.maxScrollHeight) // Outside the scroll view on purpose. Inside it the composer was one more row // in a list that can be taller than the rail — it could open scrolled out of // sight, and it moved under the pointer as posts arrived. Pinned here it is