{
  "version": "https://jsonfeed.org/version/1.1",
  "title": "Bruce Blog",
  "description": "Design, product, interaction, engineering and decisions. Notes on building, for developers and the agents working with them.",
  "home_page_url": "https://bruce.flydog.ai/blog",
  "feed_url": "https://bruce.flydog.ai/blog/feed.json",
  "language": "en",
  "items": [
    {
      "id": "https://bruce.flydog.ai/blog/why-we-build-in-public",
      "url": "https://bruce.flydog.ai/blog/why-we-build-in-public?lang=en",
      "title": "Leave a trail for the next developer",
      "summary": "The founding issue of the Bruce blog: design, product, interaction, engineering and decisions, written so developers and their agents can build on the work.",
      "content_html": "<h1>Leave a trail for the next developer</h1><p>The founding issue of the Bruce blog: design, product, interaction, engineering and decisions, written so developers and their agents can build on the work.</p><p>The Bruce team · Published <time datetime=\"2026-11-05\">2026-11-05</time></p><section id=\"everyone-who-builds\"><h2>People who use the tools are becoming developers</h2><p>We build Bruce to help more people turn ideas into things they can use. Independent makers, first-time product builders and professional teams arrive with different experience. Once they start asking questions, testing ideas and judging results, they are taking part in development.</p><p>AI can handle more of the code. People still decide why a product should work a certain way, what feels awkward and which experiments failed. Those decisions often live in conversations, changes and commits. The project moves forward; its reasoning can disappear behind it.</p></section><section id=\"leave-a-trail\"><h2>Make the work a path someone else can follow</h2><p>This journal begins with a commitment to record Bruce&#39;s design, product, interaction, engineering explorations and important decisions. A useful article explains the original problem, what we observed, what we tried, what we chose and what remains unresolved.</p><p>That is one form of Build in Public. A finished result is only part of the story. A detour, a disproved hypothesis or a measurement that only makes sense in its real setting may save the next developer a wrong turn. We write for people and for the agents helping them. Clear context, constraints and verification make knowledge easier to use again.</p></section><section id=\"from-work-to-writing\"><h2>Let development grow its own journal</h2><p>This is also something Bruce aims to help its users do. Its existing built-in writing prompts can gather relevant memories, milestones and records around a topic, use available Git history, and organize blog posts, development diaries, progress reports and Build-in-Public drafts.</p><p>Finding material and drafting text are followed by human review: check facts, add explanations, remove information that should stay private, then decide whether to save and publish. That decision matters. Writing assistance should help you express your work rather than announce achievements you have not verified.</p></section><section id=\"a-place-to-talk\"><h2>Give technical conversations somewhere concrete to begin</h2><p>We do not expect every article to offer the only answer. We want discussion grounded in a particular situation. Why did you choose a different approach? How does the same interaction behave on your device? What did your agent notice that we missed?</p><p>Articles have stable addresses, linkable sections and a text version you can keep. Share one with a colleague or an agent, or email us about a specific article. Starting from work that actually happened is easier than reconstructing all of its context.</p></section><section id=\"explain-the-product\"><h2>Use one article to explain the work more than once</h2><p>A short feature description should have solid reasoning behind it. The blog gives that reasoning room: why an interaction is arranged this way, what a technical decision buys us, and how we balance a light tool with a complete experience. Product pages can link to the relevant article when readers want more depth.</p><p>Reviewed articles can also become source material for contributed pieces, product introductions and conversations with the media. We will keep the full explanation, then adapt summaries for their setting. Public communication and actual work should share the same evidence; future readers should be able to understand the choices we made.</p></section><section id=\"start-here\"><h2>Begin here, then recover the path behind us</h2><p>Our first collection includes this founding issue, the puppy identity story, two interaction investigations and an engineering explanation of the Agent&#39;s runtime boundary. Older accounts retain their event dates alongside their publication date. Historical measurements keep their test conditions.</p><p>We will gradually turn more of the project&#39;s documents and commit history into articles worth reading. Design, product, interaction, engineering and decisions each have stories to tell. We hope an experience you find here helps your next experiment, and that these records introduce us to more people who are making things.</p></section>",
      "date_published": "2026-11-05T00:00:00.000Z",
      "authors": [
        {
          "name": "The Bruce team"
        }
      ],
      "tags": [
        "editorial"
      ],
      "language": "en"
    },
    {
      "id": "https://bruce.flydog.ai/blog/iphone-duo-one-night",
      "url": "https://bruce.flydog.ai/blog/iphone-duo-one-night?lang=en",
      "title": "One night with iPhone Duo: measure first, then let the system lead",
      "summary": "Two displays, one hinge, six poses, and screenshots due by April 2027. We measured the Bruce iPhone app in every state, removed its drawn headers, kept content off the fold, and wrote down every wrong turn.",
      "content_html": "<h1>One night with iPhone Duo: measure first, then let the system lead</h1><p>Two displays, one hinge, six poses, and screenshots due by April 2027. We measured the Bruce iPhone app in every state, removed its drawn headers, kept content off the fold, and wrote down every wrong turn.</p><p>Yang Yong’an + Claude Code · Published <time datetime=\"2026-11-13\">2026-11-13</time> · Originally recorded <time datetime=\"2026-11-13\">2026-11-13</time></p><section id=\"deadline\"><h2>A new form factor with a deadline</h2><p>iPhone Duo reached customers on October 23, 2026. It folds like a passport: closed, you use the outer cover display; open, a larger inner display; and it can stand half open on a table. Apple&#39;s developer news is direct about it: recompile with Xcode 27.1, check every pose and orientation, and submit iPhone Duo screenshots, and the product page shows that the app is optimized for iPhone Duo. Starting April 2027, submissions need Duo screenshots.</p><p>Bruce&#39;s iPhone app is a remote for the agents on your computer: read sessions, answer questions, approve actions, open a terminal. It was laid out for one rectangle per device. This is how we and Claude Code adapted it to the new display in one evening: measure, decide, then verify state by state.</p><p>The screenshot sizes are in the specification: the outer display at 1398 × 2034 or 2034 × 1398 pixels, the inner at 2007 × 2853 or 2853 × 2007. They are exactly the Simulator&#39;s two displays.</p><ul class=\"article-sources\"><li><a href=\"https://developer.apple.com/news/?id=kkphp5qo\" rel=\"noopener noreferrer\">Apple Developer · Prepare and submit your apps for iPhone Duo</a></li><li><a href=\"https://developer.apple.com/help/app-store-connect/reference/app-information/screenshot-specifications/\" rel=\"noopener noreferrer\">App Store Connect Help · Screenshot specifications</a></li><li><a href=\"https://developer.apple.com/iphone-duo/\" rel=\"noopener noreferrer\">Apple Developer · iPhone Duo</a></li><li><a href=\"https://developer.apple.com/iphone-duo/prepare/\" rel=\"noopener noreferrer\">Apple Developer · Three steps to make your app shine on iPhone Duo</a></li></ul></section><section id=\"measure\"><h2>Measure all six states first</h2><p>We changed no code at first. We added a debug probe that reads the window, orientation, size classes, safe area, and the reserved regions and hinge state that iOS 27.1 adds. An app cannot turn itself on the Duo, and the Simulator cannot be folded from the command line, so a person folded and turned it while a small script read the probe every two seconds and saved the readings and a screenshot whenever the state changed.</p><p>Some readings surprised us. Folded, the status area is an 84 × 174 rectangle in the top corner, yet the safe area gives up the whole edge. Half open, the hinge is a 40-point division with 20 points on each side of the fold; fully open, the division is still reported, only inactive. One fact mattered later: even with the window on the inner display, UIScreen.main reports the 466 × 678 cover display.</p><div class=\"journal-figures\"><figure class=\"journal-figure\"><a class=\"journal-figure-media\" href=\"https://bruce.flydog.ai/blog/media/iphone-duo/states.en.svg\" target=\"_blank\" rel=\"noopener noreferrer\"><picture><img src=\"https://bruce.flydog.ai/blog/media/iphone-duo/states.en.svg\" width=\"1000\" height=\"650\" alt=\"The six states: folded upright 466×678, folded landscape 678×466, open upright 669×951, open landscape 951×669, half open upright and half open landscape, marking the safe area, the corner occlusion, and the division active when half open and inactive when fully open.\" loading=\"lazy\" decoding=\"async\"></picture></a><figcaption>Figure 1. The six states, each measured. Numbers from the iOS 27.1 reserved regions and hinge, in points.</figcaption></figure></div></section><section id=\"system-bars\"><h2>First wrong turn: reclaiming the empty strip</h2><p>Folded and upright, the app used 382 of the display&#39;s 466 points; the strip under the status area stayed empty. Our first reflex was to run the lists into it. Alignment broke, and Apple&#39;s iPhone Duo design guidance explained why: that strip is where the system puts toolbars and tab bars on the outer display.</p><p>The real problem was our drawn header: segmented tabs, search and a new button across the top, leaving the system nothing to place on the side. We reverted that night and moved to the system&#39;s TabView and toolbars: Conversations, Projects and Devices, plus a system search tab, each with its actions as toolbar items. Folded, the system lays them down the trailing strip by itself.</p><p>It also settled a rule we now apply everywhere: <mark>each platform&#39;s own way first</mark>. A native feel and the platform&#39;s features come before making the phones look alike; concepts, words and behavior stay one product, and the chrome follows each platform.</p><div class=\"journal-figures journal-figures-pair\"><figure class=\"journal-figure\"><a class=\"journal-figure-media\" href=\"https://bruce.flydog.ai/blog/media/iphone-duo/folded-before.webp\" target=\"_blank\" rel=\"noopener noreferrer\"><picture><img src=\"https://bruce.flydog.ai/blog/media/iphone-duo/folded-before.webp\" width=\"699\" height=\"1017\" alt=\"Before, the home on the folded cover display: drawn segmented tabs, search and a new button across the top, with an empty strip down the right edge under the status area.\" loading=\"lazy\" decoding=\"async\"></picture></a><figcaption>Figure 2. Before. A drawn header across the top, the right strip empty.</figcaption></figure><figure class=\"journal-figure\"><a class=\"journal-figure-media\" href=\"https://bruce.flydog.ai/blog/media/iphone-duo/folded-after.webp\" target=\"_blank\" rel=\"noopener noreferrer\"><picture><img src=\"https://bruce.flydog.ai/blog/media/iphone-duo/folded-after.webp\" width=\"699\" height=\"1017\" alt=\"After, the home on the folded cover display: the system&#39;s large title, the account button at the top right, and the system tab bar running down the right strip.\" loading=\"lazy\" decoding=\"async\"></picture></a><figcaption>Figure 3. After. The system puts the tab bar in the strip.</figcaption></figure></div></section><section id=\"tabs\"><h2>Tabs are navigation, not tools</h2><p>On the inner display held landscape, the system also turned the tab bar into a column of icons on the right. Looking at the screenshot, the product call was quick: the three tabs are the app&#39;s navigation, and without their labels they lose their meaning; they are not tool buttons. We looked at how large apps handle wide screens and tried the system&#39;s sidebarAdaptable sidebar first. Once collapsed, it offered no way back, and remembered that state.</p><p>The final choice is more restrained: where the window is wide and not split at a hinge, iOS 27.1&#39;s toolbarVerticalBehavior(.disabled) keeps the bars horizontal, and the tab bar returns to the bottom with its titles. The cover display and the half-open split keep the system&#39;s side bars.</p><div class=\"journal-figures\"><figure class=\"journal-figure\"><a class=\"journal-figure-media\" href=\"https://bruce.flydog.ai/blog/media/iphone-duo/open-landscape.webp\" target=\"_blank\" rel=\"noopener noreferrer\"><picture><img src=\"https://bruce.flydog.ai/blog/media/iphone-duo/open-landscape.webp\" width=\"1400\" height=\"984\" alt=\"The inner display open in landscape: the list of conversations on the left, an agent&#39;s session on the right, and the system tab bar along the bottom with titled tabs for conversations, projects, devices and search.\" loading=\"lazy\" decoding=\"async\"></picture></a><figcaption>Figure 4. Open in landscape. List on the left, page on the right, tabs with titles.</figcaption></figure></div></section><section id=\"headers\"><h2>Hand page headers to the system bar</h2><p>Next came the pages. The session page also drew its own header: Back, an avatar, the title and a few buttons in one row across the top. In the system bar, the avatar and title are now the principal item, and tapping the title still opens the agent, the project and the computer; Back, the terminal and the side pane are bar items, which go down the side with the system&#39;s bars on the cover display.</p><p>Three surprises came with it. First, the detail column&#39;s bar stood empty: the page&#39;s toolbar sat inside an .inspector and never reached the navigation bar, so the inspector moved outside the stack. Second, the transcript is an upside-down scroller, for scrolling performance on long sessions, and cannot use the system&#39;s scroll edge effect, so text showed through the bar. A bar background was not enough: with side bars the title&#39;s band floats 14 points below the top, and the text stayed sharp above it. We fixed it at the source, with a mask that stops the transcript under the top bar while it still runs under the box at the bottom. Third, the side pane&#39;s on and off state became the system&#39;s toggle button.</p></section><section id=\"half-open\"><h2>Half open: read above, touch below</h2><p>Half open and upright, the hinge crosses the window between 456 and 496 points. We wrote a FoldedColumn: the transcript ends above the fold, and the background-task strip, the question trays, the box and the terminal&#39;s keys start just under it. There is only ever one structure, and only its sizes change with the hinge, so folding never loses the transcript or its scroll position.</p><p>The photo viewer centered its photo, right on the fold. Now the photo sits above it, with the close button and the count just below. Half-height sheets needed nothing from us: measured, the system already places their top edge at 487.5 points, under the fold.</p><div class=\"journal-figures journal-figures-pair\"><figure class=\"journal-figure\"><a class=\"journal-figure-media\" href=\"https://bruce.flydog.ai/blog/media/iphone-duo/half-open-session.webp\" target=\"_blank\" rel=\"noopener noreferrer\"><picture><img src=\"https://bruce.flydog.ai/blog/media/iphone-duo/half-open-session.webp\" width=\"700\" height=\"995\" alt=\"An agent&#39;s session half open and upright: the transcript ends above the fold, and the background-task strip, a note and the box begin under it. Two cyan dashed lines mark the hinge between 456 and 496 points.\" loading=\"lazy\" decoding=\"async\"></picture></a><figcaption>Figure 5. A session half open. The dashed lines are added to mark the hinge.</figcaption></figure><figure class=\"journal-figure\"><a class=\"journal-figure-media\" href=\"https://bruce.flydog.ai/blog/media/iphone-duo/half-open-photo.webp\" target=\"_blank\" rel=\"noopener noreferrer\"><picture><img src=\"https://bruce.flydog.ai/blog/media/iphone-duo/half-open-photo.webp\" width=\"700\" height=\"995\" alt=\"The photo viewer half open and upright: the photo is whole above the fold, with the close button and the count of one of two just below it. Cyan dashed lines mark the hinge.\" loading=\"lazy\" decoding=\"async\"></picture></a><figcaption>Figure 6. The photo viewer. Photo above, bar below the fold.</figcaption></figure></div></section><section id=\"keyboard\"><h2>A Simulator without a keyboard, and a stand-in</h2><p>Does the box still fit under the fold once the keyboard is up? That needed measuring, but this Simulator runs headless: focusing a field brings up no software keyboard, and there is no window for Command-K. So the debug channel gained a stand-in keyboard that takes a given height off the bottom of the page, the way the keyboard&#39;s safe area does.</p><p>At 200 points the box stays under the fold; at 336 there is no room, and it rises to sit just above the fold. The first version got this wrong: it measured its own height as the sum of its parts, kept the old value when the keyboard came up, and pushed the transcript behind the title bar. Measuring the room it is given fixed it, and all four states passed again.</p><div class=\"journal-figures\"><figure class=\"journal-figure\"><a class=\"journal-figure-media\" href=\"https://bruce.flydog.ai/blog/media/iphone-duo/keyboard.webp\" target=\"_blank\" rel=\"noopener noreferrer\"><picture><img src=\"https://bruce.flydog.ai/blog/media/iphone-duo/keyboard.webp\" width=\"1728\" height=\"597\" alt=\"Four half-open captures side by side: with no keyboard the box is under the fold; with a 200-point stand-in keyboard it stays under; at 336 points it rises above the fold; with the keyboard gone the page is back as it was. Cyan dashed lines mark the hinge.\" loading=\"lazy\" decoding=\"async\"></picture></a><figcaption>Figure 7. Left to right: no keyboard, 200 points, 336 points, put away. The gray block is the stand-in.</figcaption></figure></div></section><section id=\"window\"><h2>The window, not the screen</h2><p>The keyboard hid an older problem. The code compared the keyboard&#39;s position with the height of UIScreen.main to decide whether it was up. On the Duo, the main screen is the 678-point cover while the window is on the 951-point inner display: a keyboard on the inner display with its top at 700 read as down.</p><p>The fix uses the screen that comes with the keyboard&#39;s own notification and converts the frame into the points of the window it covers. To check it, the debug channel can now send a keyboard notification, and a keyboard at 700 reads as up. The other code using the main screen, a preview zoom width, turned out to have no caller and was deleted. The app no longer uses UIScreen.main at all.</p></section><section id=\"fold-mid-use\"><h2>Fold mid-use, and the page follows</h2><p>Folded and open upright are narrow layouts, with pages pushed on the list&#39;s stack; open in landscape is wide, with the page beside the list. What happens if you fold or open the device in the middle of a session? A debug switch that flips between wide and narrow showed two problems. Widening left a pushed page in the 360-point list column with the detail empty. Narrowing collapsed the split onto its detail, and the system bar showed Back but lost the page&#39;s title.</p><p>Now a page always lives where its width puts pages: widening moves it into the detail, narrowing moves it back onto the stack. While a terminal is open over it, it stays put until the terminal closes. The connection belongs to the shell, not the page, so nothing reconnects; there and back twice, the session on screen and the draft in the box were still there.</p><div class=\"journal-figures\"><figure class=\"journal-figure\"><a class=\"journal-figure-media\" href=\"https://bruce.flydog.ai/blog/media/iphone-duo/fold-move.webp\" target=\"_blank\" rel=\"noopener noreferrer\"><picture><img src=\"https://bruce.flydog.ai/blog/media/iphone-duo/fold-move.webp\" width=\"1728\" height=\"597\" alt=\"Four captures: switched wide, the session moves into the detail column with the list over the left; switched narrow, it returns to the stack with its title and draft; a second round trip gives the same result.\" loading=\"lazy\" decoding=\"async\"></picture></a><figcaption>Figure 8. Wide, narrow, wide, narrow. The page, its title and the draft stay.</figcaption></figure></div></section><section id=\"search\"><h2>Search, finished along the way</h2><p>With the home on system tabs, search became the system&#39;s search tab: one field across devices, projects and conversations, results grouped under each tab&#39;s name, each group drawn with that tab&#39;s own rows. Devices and projects match on the phone as you type; conversations are each computer&#39;s own search, asked a moment after the last key. When nothing matches anywhere, the system&#39;s own no-results view appears.</p><div class=\"journal-figures journal-figures-pair\"><figure class=\"journal-figure\"><a class=\"journal-figure-media\" href=\"https://bruce.flydog.ai/blog/media/iphone-duo/search-light.webp\" target=\"_blank\" rel=\"noopener noreferrer\"><picture><img src=\"https://bruce.flydog.ai/blog/media/iphone-duo/search-light.webp\" width=\"700\" height=\"995\" alt=\"The search tab in light mode searching for bruce: a devices group with two test computers and two offline ones, a projects group with the bruce project on two computers, then open conversations.\" loading=\"lazy\" decoding=\"async\"></picture></a><figcaption>Figure 9. Results grouped into devices, projects and conversations (light).</figcaption></figure><figure class=\"journal-figure\"><a class=\"journal-figure-media\" href=\"https://bruce.flydog.ai/blog/media/iphone-duo/search-dark.webp\" target=\"_blank\" rel=\"noopener noreferrer\"><picture><img src=\"https://bruce.flydog.ai/blog/media/iphone-duo/search-dark.webp\" width=\"700\" height=\"995\" alt=\"The same search in dark mode: devices, projects and conversations, with the list scrolling across the fold the way the system&#39;s own lists do.\" loading=\"lazy\" decoding=\"async\"></picture></a><figcaption>Figure 10. The same search, dark.</figcaption></figure></div></section><section id=\"tools\"><h2>Make every state measurable</h2><p>The most useful result of the night was not any single fix but the tools that made each state measurable. With no device in hand and a Simulator that cannot be folded from a script, these debug switches took us to every state, and each conclusion came with a screenshot and a set of readings.</p><ul><li>A probe: the window, size classes, safe area, occlusions, the division and the hinge in one reading.</li><li>A side-bars switch: the system&#39;s vertical bars on the inner display, as when folded.</li><li>A fake hinge: an active division on any display, set at launch.</li><li>A stand-in keyboard and a keyboard notification: the keyboard measured on a Simulator that has none.</li><li>A width switch: wide and narrow layouts, as folding would change them.</li><li>One capture script: nine pages, light and dark, in the current state.</li><li>Source locks: no UIScreen.main, the division read from the system, pages moving with the width. Each decision is guarded by a test.</li></ul></section><section id=\"left\"><h2>What is not done yet</h2><p>We ran the full capture only in one real state, half open and upright. Folded, open flat in both orientations, and half open in landscape still need someone to set the device and run it again. Folding mid-dictation is expected to stop the dictation, since the box is drawn again when the page moves; folding mid-terminal, and the terminal and file pages, need a live computer.</p><p>The build side of the badge is met: Bruce builds with the iPhoneOS 27.2 SDK, supports portrait and both landscapes, and does not require full screen. What remains is a set of screenshots with real content for each display, uploaded in App Store Connect.</p></section><section id=\"checklist\"><h2>A checklist for other teams</h2><p>If you are working toward the same April 2027 deadline, this is the order we would follow again.</p><ul><li>Rebuild with the iOS 27.1 SDK or later; check full-screen settings and supported orientations.</li><li>Measure all six states before changing anything, and keep the numbers with the screenshots.</li><li>Replace drawn headers and tab strips with system bars, and let the system place them on each display.</li><li>Read the division in the view that lays out around it; while it is active, keep text and controls off it.</li><li>Take sizes from the window and the view&#39;s geometry, never UIScreen.main.</li><li>Decide where a page lives at each width, and move it when the width changes mid-use.</li><li>Keep the system&#39;s sheet placement unless you measure a problem.</li><li>Capture the outer and inner displays at their native sizes for App Store Connect.</li></ul><ul class=\"article-sources\"><li><a href=\"https://developer.apple.com/news/?id=kkphp5qo\" rel=\"noopener noreferrer\">Apple Developer · Prepare and submit your apps for iPhone Duo</a></li><li><a href=\"https://developer.apple.com/help/app-store-connect/reference/app-information/screenshot-specifications/\" rel=\"noopener noreferrer\">App Store Connect Help · Screenshot specifications</a></li><li><a href=\"https://developer.apple.com/iphone-duo/\" rel=\"noopener noreferrer\">Apple Developer · iPhone Duo</a></li><li><a href=\"https://developer.apple.com/iphone-duo/prepare/\" rel=\"noopener noreferrer\">Apple Developer · Three steps to make your app shine on iPhone Duo</a></li></ul></section>",
      "date_published": "2026-11-13T00:00:00.000Z",
      "authors": [
        {
          "name": "Yang Yong’an + Claude Code"
        }
      ],
      "tags": [
        "engineering"
      ],
      "language": "en",
      "_bruce_event_date": "2026-11-13"
    },
    {
      "id": "https://bruce.flydog.ai/blog/one-prompt-nine-agents",
      "url": "https://bruce.flydog.ai/blog/one-prompt-nine-agents?lang=en",
      "title": "One prompt, nine agents: grading Bruce’s animated stickers",
      "summary": "Nine coding agents received the same one-sentence brief, with no follow-ups or retries. We checked the delivered SVGs in a real browser and ran an anonymous blind review. The costliest mistakes were the silent ones.",
      "content_html": "<h1>One prompt, nine agents: grading Bruce’s animated stickers</h1><p>Nine coding agents received the same one-sentence brief, with no follow-ups or retries. We checked the delivered SVGs in a real browser and ran an anonymous blind review. The costliest mistakes were the silent ones.</p><p>Yang Yong’an + Claude Code · Published <time datetime=\"2026-11-11\">2026-11-11</time> · Originally recorded <time datetime=\"2026-11-11\">2026-11-11</time></p><section id=\"the-brief\"><h2>A one-sentence brief</h2><p>On 11 November we gave nine coding agents the same sentence: using Bruce’s original brand artwork, and taking a study of Grok Bot’s animated emoji as a reference, make a set of SVG animated stickers with a design write-up and a preview page, in a folder named after the model, version and effort level. It asked for real SVG animation with complete animation frames, for use while waiting, when celebrating and in blog illustrations.</p><p>There was one rule: <strong>send that sentence and nothing else</strong>. No questions answered, no additions, no retries. Even when a session looked one “continue” away from finishing, we scored what existed at that point. This compares a tool, a model and its configuration on a single attempt. It is a sample, <em class=\"journal-wavy\">not a general verdict on any model</em>.</p><ul><li>Codex · gpt-6-astra</li><li>ZCode · GLM-5.3</li><li>Kimi Code · underlying model not reported by the session</li><li>Grok CLI · grok-4.7-build-fast</li><li>Qwen Code · qwen4-max-preview</li><li>DeepSeek Harness · deepseek-v4-pro</li><li>WorkBuddy 5.3.12 · Tencent Hunyuan Hy4 (its folder is named claude-opus-6-high; see below)</li><li>MiMo Code 0.1.16 · mimo-v2.6-pro (no delivery)</li><li>Claude Code · claude-opus-5-5 (the judge for this article; its entry is not ranked)</li></ul></section><section id=\"snapshot\"><h2>Snapshot first, then judge</h2><p>Four agents committed their work themselves, following the project’s conventions; four left their files in the working tree. Before judging, we committed those four once under each agent’s own tool and model name, <strong>without changing a file</strong>, as the snapshot the review is based on. MiMo Code produced no files, so there was nothing to commit.</p><p>Committing counted toward the score. The project asks agents to commit promptly once a task passes its checks, which is direct evidence of whether an agent reads and follows a project’s working conventions. It carries only a small weight.</p></section><section id=\"how-we-judged\"><h2>How we judged</h2><p>Claude Code did the judging, and it was also a contestant. To avoid marking its own work, its entry is scored on the same scale and the score is published, but <em class=\"journal-wavy\">it is not ranked</em>.</p><p>Scores lean on reproducible measurements. Chrome loaded <mark>each entry’s delivered SVG files</mark> and paused them at evenly spaced moments of their loops. Every animation’s keyTimes, keySplines and values were checked for validity. We looked for embedded bitmaps, scripts or external references, and for ids that collide when several files share a page. Each preview page was opened at 1280 and 390 pixels wide to record errors and horizontal overflow.</p><p>We also ran an anonymous blind review. The eight contact sheets were stripped of names, shuffled and given to another model to score brand fidelity, expressiveness, visible motion, craft and product usefulness. It belongs to the same model family as the judge, so its result is a reference only.</p><ul><li>Brief and conventions, 15 points: design write-up, preview page, folder name, committing per project conventions, honest self-identification.</li><li>Brand fidelity, 20 points: is it still the owner-approved, left-facing pilot husky?</li><li>Animation, 25 points: distinct emotions, anticipation, overshoot and squash and stretch, and real change from frame to frame.</li><li>Engineering, 25 points: valid animation, self-contained files, size, id isolation and a robust preview.</li><li>Product fit, 15 points: scene coverage, small sizes, light and dark surfaces, reduced motion and embedding guidance.</li></ul></section><section id=\"results\"><h2>Results</h2><p><strong>Codex ranks first with 77</strong>, Kimi Code second with 74, ZCode third with 71, and WorkBuddy with Hunyuan fourth with 67. Qwen Code scores 59, Grok CLI 50 and DeepSeek Harness 48. MiMo Code delivered nothing. The judge’s own entry scores 87 on the same scale and is not ranked; in the blind review it tied with the Hunyuan entry for the highest total.</p><p>The top four differ mostly in trade-offs: some redrew the character to gain exaggerated motion, others kept the brand but changed the face little. The bottom three have more specific problems, described below.</p><div class=\"journal-figures\"><figure class=\"journal-figure\"><a class=\"journal-figure-media\" href=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/scoreboard.en.svg\" target=\"_blank\" rel=\"noopener noreferrer\"><picture><source media=\"(max-width:640px)\" srcset=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/scoreboard-narrow.en.svg\" width=\"520\" height=\"954\"><img src=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/scoreboard.en.svg\" width=\"1000\" height=\"520\" alt=\"Horizontal stacked bars: Codex 77, Kimi Code 74, ZCode 71, WorkBuddy Hunyuan 67, Qwen Code 59, Grok CLI 50, DeepSeek Harness 48, MiMo Code no delivery, and the judge’s entry at 87, not ranked.\" loading=\"lazy\" decoding=\"async\"></picture></a><figcaption>Figure 1. Overall scores; the five colors are the five criteria. The judge’s entry is shown faded and is not ranked.</figcaption></figure></div></section><section id=\"codex\"><h2>Codex · gpt-6-astra: the most ready-to-use set</h2><p>Instead of the profile master, it redrew a full-body chibi Bruce. The palette, goggles and violet badge remain, while a body, a tail and props such as a light bulb, a check mark and a laptop make its 12 stickers easy to tell apart. <mark>All 488 SMIL animations are valid</mark> and files average 23.7KB. The preview page is fully designed, does not overflow at 390 pixels and logs no errors. It also committed a plan before committing the work, following the project’s long-task conventions throughout.</p><p>It loses points on the brand. <strong>The full body is not the owner-approved master</strong>, the character fills little of the frame, and the face is hard to read at small sizes. SMIL animation also cannot respond to the system’s reduced-motion setting from inside the file.</p><div class=\"journal-figures\"><figure class=\"journal-figure\"><a class=\"journal-figure-media\" href=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-gpt.svg\" target=\"_blank\" rel=\"noopener noreferrer\"><picture><img src=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-gpt.svg\" width=\"1010\" height=\"340\" alt=\"Three stickers from the Codex entry: a full-body chibi Bruce waiting, jumping in celebration with confetti, and leaning back with a frown after a mistake.\" loading=\"lazy\" decoding=\"async\"></picture></a><figcaption>Figure 2. Codex: waiting, celebrating, oops. A full body lets the motion use the body and props. The figure omits an edge filter from the original files; the last section explains why.</figcaption></figure></div><p class=\"journal-artifacts\"><span>Deliverables</span><a href=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entries/gpt-6-astra-ultra/index.html\" rel=\"noopener noreferrer\">Open this entry’s preview page</a></p></section><section id=\"kimi\"><h2>Kimi Code: the most restrained, and the steadiest</h2><p>It reuses the master’s paths and sets Bruce on a pale disc. Its 10 stickers change mainly through whole-body bounces and squashes, plus overlays such as a question mark, hearts, a warning sign and a check mark. All 196 animations are valid, files are small and the preview page is strongly designed. The word unspecified in its folder name records that the session gave no verifiable model version; <mark>it said so instead of inventing one</mark>.</p><p><strong>The face barely changes.</strong> Eyes and mouth stay as in the master, emotion comes mostly from the symbols around it, and the exaggeration falls short. The entry was not committed per the project’s conventions.</p><div class=\"journal-figures\"><figure class=\"journal-figure\"><a class=\"journal-figure-media\" href=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-kimi.svg\" target=\"_blank\" rel=\"noopener noreferrer\"><picture><img src=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-kimi.svg\" width=\"1010\" height=\"340\" alt=\"Three stickers from the Kimi Code entry: the unchanged Bruce head with thinking dots, a celebration ring of confetti, and a red warning triangle.\" loading=\"lazy\" decoding=\"async\"></picture></a><figcaption>Figure 3. Kimi Code: thinking, celebrating, error. The master stays intact; emotion lives in the symbols around it.</figcaption></figure></div><p class=\"journal-artifacts\"><span>Deliverables</span><a href=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entries/kimi-code-unspecified-xhigh/index.html\" rel=\"noopener noreferrer\">Open this entry’s preview page</a></p></section><section id=\"glm\"><h2>ZCode · GLM-5.3: the most exaggerated, and the furthest from the master</h2><p>It drew a big-headed, front-facing chibi. Its 9 stickers use CSS animation with convincing squash and stretch, ears that burst outward and speed lines; the blind review gave it <mark>the top expressiveness score</mark>. Each file is about 9KB and handles reduced motion internally, which SMIL cannot do inside a file.</p><p>It is also <strong>the furthest from the brand</strong>. The face looks forward rather than left. The badge’s double chevron is drawn as two opposing points that read as an “×” at small sizes. Ears are cropped by the canvas in some frames, and the preview page overflows horizontally at 390 pixels.</p><div class=\"journal-figures\"><figure class=\"journal-figure\"><a class=\"journal-figure-media\" href=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-glm.svg\" target=\"_blank\" rel=\"noopener noreferrer\"><picture><img src=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-glm.svg\" width=\"1010\" height=\"340\" alt=\"Three stickers from the ZCode entry: a big-headed front-facing Bruce waiting with paws raised, laughing with its head thrown back, and startled with its ears flaring.\" loading=\"lazy\" decoding=\"async\"></picture></a><figcaption>Figure 4. ZCode: waiting, celebrating, startled. The most exaggerated set, drawn away from the master.</figcaption></figure></div><p class=\"journal-artifacts\"><span>Deliverables</span><a href=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entries/glm-5.3-xhigh/index.html\" rel=\"noopener noreferrer\">Open this entry’s preview page</a></p></section><section id=\"hunyuan\"><h2>WorkBuddy · Hunyuan Hy4: the closest to a Telegram sticker</h2><p>It draws Bruce as a front-facing chibi on a rounded ice-blue plate and keeps the &gt;&gt; badge. Its 12 stickers have <mark>the richest faces</mark>: crossed-out eyes, stars, heart eyes, a startled stretch and confetti. Every animation track is written into SMIL frame by frame at 30fps, 949 tracks in all, every one valid, with a frame-inspection panel in the preview.</p><p>The cost is <strong>size</strong>: single files run from 80 to 466KB, about 11KB on average once gzipped, and the preview overflows on narrow screens. The entry was not committed per the project’s conventions.</p><div class=\"journal-figures\"><figure class=\"journal-figure\"><a class=\"journal-figure-media\" href=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-hunyuan.svg\" target=\"_blank\" rel=\"noopener noreferrer\"><picture><img src=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-hunyuan.svg\" width=\"1010\" height=\"340\" alt=\"Three stickers from the WorkBuddy Hunyuan entry: a front-facing Bruce on an ice-blue rounded plate, circled by a progress arc while waiting, stretched when startled, and with heart eyes when in love.\" loading=\"lazy\" decoding=\"async\"></picture></a><figcaption>Figure 5. WorkBuddy · Hunyuan: waiting, startled, love. The richest faces, and the largest files.</figcaption></figure><p class=\"journal-figure-after\">Its identity deserves a note more than its faces do. The brief asked for a folder named after the model, version and effort level. The agent named its folder claude-opus-6-high and described that as “this session’s runtime identifier”, while the run actually used <em class=\"journal-wavy\">Hunyuan Hy4 in WorkBuddy</em>. We kept the folder name, recorded the real model in the commit message and took points off under conventions. The owner’s screenshot of the session, below, shows both names at once.</p><figure class=\"journal-figure\"><a class=\"journal-figure-media\" href=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/workbuddy-hy4.png\" target=\"_blank\" rel=\"noopener noreferrer\"><picture><img src=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/workbuddy-hy4.png\" width=\"1600\" height=\"1148\" alt=\"WorkBuddy session screenshot: the prompt asks for a folder named after model, version and effort; the reply says the work is in claude-opus-6-high; the model picker at the bottom shows Hy4; the preview page is on the right.\" loading=\"lazy\" decoding=\"async\"></picture></a><figcaption>Figure 6. The WorkBuddy session, as captured by the owner. Hunyuan Hy4 is selected at the bottom, yet the reply names the folder claude-opus-6-high. The sidebar is cropped and local paths are pixelated.</figcaption></figure></div><p class=\"journal-artifacts\"><span>Deliverables</span><a href=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entries/claude-opus-6-high/index.html\" rel=\"noopener noreferrer\">Open this entry’s preview page</a></p></section><section id=\"qwen\"><h2>Qwen Code: a thorough preview, a third of the animation never plays</h2><p>It reuses the master and redraws rounder chibi eyes. The preview page works like a small studio, with a permanent control panel, frame stepping, an embedding guide and a bundled download. The entry was committed per the project’s conventions.</p><p>The problem is in the delivered SVGs: <strong>51 of 158 animations, across 9 files, have keyTimes that do not end at 1</strong>. Chrome drops those animations without even logging them, so the hearts in “love” and the smoke in “launch” never appear. Its own report of a clean console was accurate; this kind of failure is <em class=\"journal-wavy\">simply silent</em>.</p><div class=\"journal-figures\"><figure class=\"journal-figure\"><a class=\"journal-figure-media\" href=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-qwen.svg\" target=\"_blank\" rel=\"noopener noreferrer\"><picture><img src=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-qwen.svg\" width=\"1010\" height=\"340\" alt=\"Three stickers from the Qwen Code entry: a question mark appearing while thinking, a green check mark when done, and almost no change in love.\" loading=\"lazy\" decoding=\"async\"></picture></a><figcaption>Figure 7. Qwen Code: thinking, done, love. The hearts in the third sticker are in the file but never play.</figcaption></figure></div><p class=\"journal-artifacts\"><span>Deliverables</span><a href=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entries/qwen4-max-preview-default/preview.html\" rel=\"noopener noreferrer\">Open this entry’s preview page</a></p></section><section id=\"grok\"><h2>Grok CLI: the most stickers, the least Bruce</h2><p>It delivered 20 stickers, 20 still frames and a wide writing scene, all in CSS animation with reduced-motion handling in every file, about 15KB on average. Its notes are candid about the trade-off: the master is one fused pose, so it redrew a rig that can act.</p><p><strong>The redraw did not keep Bruce.</strong> The goggles became a large cyan ring and the face is small and hard to read at small sizes. Most stickers change only the mouth and eyes slightly, and the blind review ranked it last. The entry was not committed per the project’s conventions.</p><div class=\"journal-figures\"><figure class=\"journal-figure\"><a class=\"journal-figure-media\" href=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-grok.svg\" target=\"_blank\" rel=\"noopener noreferrer\"><picture><img src=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-grok.svg\" width=\"1010\" height=\"340\" alt=\"Three stickers from the Grok CLI entry: a redrawn Bruce profile whose goggles are a large cyan ring, with little change between thinking, celebrating and surprised.\" loading=\"lazy\" decoding=\"async\"></picture></a><figcaption>Figure 8. Grok CLI: thinking, celebrating, surprised. The redrawn rig moves, but no longer looks like Bruce.</figcaption></figure></div><p class=\"journal-artifacts\"><span>Deliverables</span><a href=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entries/grok-4.7-build-fast/preview.html\" rel=\"noopener noreferrer\">Open this entry’s preview page</a></p></section><section id=\"deepseek\"><h2>DeepSeek Harness: the design is in the files, two thirds never plays</h2><p>It reuses the master and delivers 12 stickers in two versions, with and without a plate, plus GIF exports. The preview’s hero is driven live by a JavaScript spring engine and looks lively.</p><p>In the delivered SVGs, however, <strong>155 of 230 animations have invalid keyTimes</strong>, affecting all 12 files; many do not start at 0 either, and Chrome logs a parse error for those. Several files share ids, which interfere with each other when inlined on one page. The sticker cards lower on the preview page are these files, and their effects do not play there either. The entry was not committed per the project’s conventions.</p><div class=\"journal-figures\"><figure class=\"journal-figure\"><a class=\"journal-figure-media\" href=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-deepseek.svg\" target=\"_blank\" rel=\"noopener noreferrer\"><picture><img src=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-deepseek.svg\" width=\"1010\" height=\"340\" alt=\"Three stickers from the DeepSeek Harness entry: the master Bruce on an ice-blue plate, with nearly identical frames for thinking, celebrating and love.\" loading=\"lazy\" decoding=\"async\"></picture></a><figcaption>Figure 9. DeepSeek Harness: thinking, celebrating, love. Most effects never play, leaving the frames nearly still.</figcaption></figure></div><p class=\"journal-artifacts\"><span>Deliverables</span><a href=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entries/deepseek-v4-pro-harness/preview.html\" rel=\"noopener noreferrer\">Open this entry’s preview page</a></p></section><section id=\"silent-failure\"><h2>The costliest mistakes are silent</h2><p>Qwen’s and DeepSeek’s problems are the same kind. SVG animation requires keyTimes for linear and spline interpolation to start at 0 and end at 1. Stopping at 0.5 may be meant as “move in the first half, rest in the second”, but the browser <mark>treats the whole animation as an error and ignores it</mark>.</p><p>We copied each entry’s “love” sticker and only completed its keyTimes to 1, holding the last value, with nothing else changed. The hearts appeared at once. The design had been in the file all along; what was missing was <em class=\"journal-wavy\">checking the delivered file in its target environment</em>.</p><p><strong>A preview page cannot stand in for the deliverable.</strong> A JavaScript-driven preview, static screenshots and a quiet console cannot prove that a file placed in an &lt;img&gt; will move. The judge held its own entry to the same standard: structural tests check every animation, and a real browser pauses the files at chosen moments.</p><pre><code class=\"language-svg\">&lt;!-- Ignored by the browser: keyTimes stop at 0.5 --&gt;\n&lt;animate attributeName=&quot;opacity&quot; values=&quot;0;1;0&quot;\n         keyTimes=&quot;0;0.25;0.5&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;\n\n&lt;!-- Valid: complete to 1, holding the last value --&gt;\n&lt;animate attributeName=&quot;opacity&quot; values=&quot;0;1;0;0&quot;\n         keyTimes=&quot;0;0.25;0.5;1&quot; dur=&quot;2s&quot; repeatCount=&quot;indefinite&quot;/&gt;</code></pre><div class=\"journal-figures\"><figure class=\"journal-figure\"><a class=\"journal-figure-media\" href=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/keytimes.en.svg\" target=\"_blank\" rel=\"noopener noreferrer\"><picture><source media=\"(max-width:640px)\" srcset=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/keytimes-narrow.en.svg\" width=\"520\" height=\"652\"><img src=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/keytimes.en.svg\" width=\"1000\" height=\"592\" alt=\"Two rows compared: on the left, the “love” stickers as delivered by Qwen Code and DeepSeek Harness, without hearts; on the right, copies whose keyTimes were completed to 1, where the hearts appear.\" loading=\"lazy\" decoding=\"async\"></picture></a><figcaption>Figure 10. The same files with only keyTimes completed. The right column is the judge’s illustrative copy, not a contest entry.</figcaption></figure></div><ul class=\"article-sources\"><li><a href=\"https://www.w3.org/TR/SVG11/animate.html#KeyTimesAttribute\" rel=\"noopener noreferrer\">W3C SVG 1.1 · Animation: keyTimes</a></li><li><a href=\"https://developer.mozilla.org/en-US/docs/Web/SVG/Reference/Attribute/keyTimes\" rel=\"noopener noreferrer\">MDN · keyTimes</a></li></ul></section><section id=\"two-notes\"><h2>Two notes</h2><p>MiMo Code 0.1.16 with mimo-v2.6-pro stopped after about 28 minutes. Its final error was 402, “Insufficient account balance”, marked as not retryable. That is <strong>an account balance problem, not a model crash</strong>, and sending “continue” could not recover it before the balance was topped up. By then it had read the brand artwork and the reference study and was measuring face coordinates; for about three minutes it repeatedly ran empty commands while trying to call its image-reading tool. No files reached the repository. Under the one-sentence rule, it counts as no delivery.</p><p>Self-reported identities are not always reliable. Kimi Code wrote unspecified when it did not know its model, which is the honest choice. WorkBuddy reported claude-opus-6-high while the run used Hunyuan Hy4. Verifying identity <em class=\"journal-wavy\">cannot be left to the system under test</em>.</p><div class=\"journal-figures\"><figure class=\"journal-figure\"><a class=\"journal-figure-media\" href=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/mimo-402.png\" target=\"_blank\" rel=\"noopener noreferrer\"><picture><img src=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/mimo-402.png\" width=\"1600\" height=\"535\" alt=\"MiMo Code terminal screenshot: pixel-sampling output printed line by line at the top; Error · status 402 · non-retryable and Insufficient account balance in the middle; MiMo-V2.6-Pro below; 123,117 tokens and $0.09 spent on the right.\" loading=\"lazy\" decoding=\"async\"></picture></a><figcaption>Figure 11. MiMo Code when it stopped, as captured by the owner. The 402 came from the account balance, not the model; the MCP server list on the right is pixelated.</figcaption></figure></div></section><section id=\"judge-entry\"><h2>The judge’s own entry</h2><p>Claude Code’s entry does not redraw Bruce. Clip paths cut the master into ears, head, jaw and body, so at rest it matches the master pixel for pixel, and a blink, an open mouth, flattened ears or goggles dropping onto the eyes all move the master itself. Its 20 stickers contain 683 animations, all valid, with a dark-surface set, still frames and 104 structural tests. It scores <strong>87</strong> on the same scale and tied for the highest total in the blind review.</p><p>Its weaknesses are clear too. The preview page is less refined than those from Codex and Kimi, and many stickers change only the face, with less squash and stretch than ZCode and Hunyuan. Because the judge and the contestant are the same agent, the score is a reference and is not ranked.</p><div class=\"journal-figures\"><figure class=\"journal-figure\"><a class=\"journal-figure-media\" href=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-claude.svg\" target=\"_blank\" rel=\"noopener noreferrer\"><picture><img src=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-claude.svg\" width=\"1010\" height=\"340\" alt=\"Three stickers from the Claude Code entry: the master Bruce thinking with a paw under its chin and a thought cloud, celebrating with a raised paw and confetti, and startled with an exclamation mark.\" loading=\"lazy\" decoding=\"async\"></picture></a><figcaption>Figure 12. The judge’s entry: thinking, celebrating, surprised. The master is cut into moving parts; not ranked.</figcaption></figure></div><p class=\"journal-artifacts\"><span>Deliverables</span><a href=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entries/claude-opus-5.5-xhigh/preview.html\" rel=\"noopener noreferrer\">Open this entry’s preview page</a></p></section><section id=\"takeaways\"><h2>What we learned</h2><p>The nine agents delivered very different work, but the ranking was usually decided less by whose idea was better than by <mark>who checked what they actually shipped</mark>.</p><ul><li>Check the deliverable in its target environment: put it in an &lt;img&gt; and pause it at chosen moments, rather than trusting the preview page.</li><li>Write structural tests for animation formats. Browsers often stay silent when SMIL is wrong.</li><li>Reusing the brand master preserves recognition best; making the face move means cutting the master into parts, not just adding symbols beside it.</li><li>CSS animation can respect reduced motion inside the file; SMIL needs separate still frames.</li><li>Verify self-reported model identities. When unsure, “not reported” beats an invented name.</li><li>One run is one sample; conclusions apply only to this tool, configuration and brief.</li></ul></section><section id=\"second-attempt\"><h2>A second attempt, with Fable 5.2</h2><p>After the review went out, the owner switched Claude Code to Fable 5.2 at max effort and asked for another entry that used what the review had taught, built on skeletal animation. <strong>This was no longer a one-sentence exam.</strong> The agent had read every conclusion above and could check its work as often as it liked, so the entry is not ranked. This section records what it did and what was measured.</p><p>The new entry still does not redraw Bruce. The master is cut into parts that hang on a tree of bones. <mark>The ears, goggles, badge and tongue are spring bones</mark>: the motion of the head and body throws them around, and nobody keys them by hand. A whole performance is simulated at 240 Hz and then baked into CSS keyframes. The 24 stickers hold 835 animations and 7,260 keyframes, and files average 44.6KB.</p><p>Because the animation is CSS, <strong>one file adapts by itself</strong>. It freezes on its most expressive frame when the system asks for reduced motion, draws an ice-blue outline around itself on dark surfaces and drops its small effects below 44 pixels. The earlier entry needed four sets of files for this, 80 in all; this one has 24.</p><p>The checking went a step further too. Besides 148 structural tests, the same delivered files were loaded in Chrome and in WKWebView, the engine of Bruce’s desktop window, paused at chosen moments and compared frame by frame. The two engines differ in at most 0.02% of pixels in any frame, and the rig at rest differs from the master in 0.044% of pixels.</p><p>Looking through a second engine paid off at once, and again the findings were silent ones. <em class=\"journal-wavy\">Chrome leaks hairlines along the seams between parts at small sizes</em>; WebKit does not. A bounds test caught two stickers whose ear tips left the canvas at the top of a jump. The bigger surprise came from rechecking the first round. The top-ranked Codex entry wraps its character in an SVG filter, and WebKit renders filtered content at a lower resolution, so in WKWebView on a 2x screen its edges are soft; without the filter they are sharp. Safari uses the same engine. The first review looked only in Chrome and missed that. The original scores stand; we record the miss here. Figure 2 omits the filter, and the contest files themselves are unchanged.</p><p>It has weaknesses of its own. It was verified only in Chrome and WKWebView, not in Firefox. Its files are a little larger than the earlier entry’s. When an ear bends to its limit, a small step shows between its two segments. The entry’s design document lists every trade-off.</p><div class=\"journal-figures\"><figure class=\"journal-figure\"><a class=\"journal-figure-media\" href=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-fable.svg\" target=\"_blank\" rel=\"noopener noreferrer\"><picture><img src=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entry-fable.svg\" width=\"1010\" height=\"340\" alt=\"Three stickers from the Fable 5.2 entry: the master Bruce thinking with a paw under its chin while dots light up in a thought cloud, leaping with both paws raised and confetti, and startled with its ears straight up and an exclamation mark.\" loading=\"lazy\" decoding=\"async\"></picture></a><figcaption>Figure 13. The second attempt: thinking, celebrating, surprised. It is still the master, but the body, ears and paws all act.</figcaption></figure><p class=\"journal-figure-after\">Taking one sticker apart shows what “skeleton” means here. On the left of the next figure is the celebrate sticker at take-off: every dot is the pivot of a bone, and the lines show which bone carries which. Nobody wrote keyframes for the violet ones; their motion is computed. On the right are six moments of the same leap.</p><figure class=\"journal-figure\"><a class=\"journal-figure-media\" href=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/fable-skeleton.svg\" target=\"_blank\" rel=\"noopener noreferrer\"><picture><img src=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/fable-skeleton.svg\" width=\"1010\" height=\"460\" alt=\"On the left, the skeleton drawn over the celebrate sticker at take-off: cyan lines join the neck, ears, jaw, goggles and badge, and violet dots mark spring bones. On the right, six moments of the same leap, from the crouch through take-off and the top to the squash on landing.\" loading=\"lazy\" decoding=\"async\"></picture></a><figcaption>Figure 14. Left: the skeleton of the celebrate sticker, with spring bones in violet. Right: six moments of one leap; the crouch, the stretch and the squash on landing all show in the silhouette.</figcaption></figure></div><p class=\"journal-artifacts\"><span>Deliverables</span><a href=\"https://bruce.flydog.ai/blog/media/one-prompt-nine-agents/entries/claude-fable-5.2-max/preview.html\" rel=\"noopener noreferrer\">Open this entry’s preview page</a></p></section>",
      "date_published": "2026-11-11T00:00:00.000Z",
      "authors": [
        {
          "name": "Yang Yong’an + Claude Code"
        }
      ],
      "tags": [
        "design"
      ],
      "language": "en",
      "_bruce_event_date": "2026-11-11"
    },
    {
      "id": "https://bruce.flydog.ai/blog/tauri-window-dragging",
      "url": "https://bruce.flydog.ai/blog/tauri-window-dragging?lang=en",
      "title": "Tracing a Tauri window dragging slowdown",
      "summary": "A multi-monitor centering bug led to blank-window controls, native parent relationships and a titlebar regression. What we measured, where the investigation went astray, and how we checked the repair.",
      "content_html": "<h1>Tracing a Tauri window dragging slowdown</h1><p>A multi-monitor centering bug led to blank-window controls, native parent relationships and a titlebar regression. What we measured, where the investigation went astray, and how we checked the repair.</p><p>Yang Yong&#39;an and Codex · Published <time datetime=\"2026-11-10\">2026-11-10</time> · Originally recorded <time datetime=\"2026-11-10\">2026-11-10</time></p><section id=\"three-problems\"><h2>Three problems in one window</h2><p>Bruce is a desktop development tool built with Tauri 2, React and Rust. The report started with multiple displays: opening Git Push did not center its dialog on the display containing the current project window.</p><p>After centering was corrected, more feedback followed. Opening still felt slow. Dragging was jerky. Settings behaved the same way. Multiple windows are a basic desktop capability, so why did an ordinary dialog feel so awkward?</p><p>We worked on position, opening latency and continuous dragging, but initially failed to accept them as separate behaviors. Seeing the window appear sooner made us underestimate the dragging problem that remained.</p></section><section id=\"negative-coordinates\"><h2>Desktop coordinates can be negative</h2><p>The old implementation clamped the computed vertical position to zero or greater. A display above the primary display can have negative coordinates: the reported project window was at y = -901. Clamping its dialog&#39;s centered position to zero pushed the dialog toward another display.</p><p>The correction preserves negative coordinates, centers against the owning window and fits the result within that display&#39;s usable area. It uses the outer frame, including the native titlebar, with consistent coordinate units.</p><pre><code class=\"language-text\">Project window: y = -901, height = 840\nDialog outer height = 674\nCentered y = -901 + (840 - 674) / 2 = -818\nPreserve the valid negative coordinate.</code></pre></section><section id=\"opening-and-dragging\"><h2>Measure opening and dragging separately</h2><p>To reduce repeated initialization, we moved Push onto the existing drawn-window mechanism. The project opens a native window and renders into its document through a React portal, sharing existing business state and the Host connection. We wait for actual stylesheet readiness and apply the content and native appearance together.</p><p>This improved opening latency, while dragging remained awkward. Opening measures time until content becomes visible. Dragging measures how closely the window follows sustained pointer input. They need separate measurements.</p><p>We had assumed native dragging would be reliable and concentrated on React, the DOM and page loading. That assumption kept the investigation at the wrong layer for too long.</p></section><section id=\"blank-window\"><h2>A blank window established the control</h2><p>The turning point was a direct request: remove all business DOM, open the simplest Tauri window and see whether it still stutters. We created an independent blank window without React or business connections, then kept the document unchanged and added only a native parent.</p><p>We injected the same 120 native drag events and recorded window position changes. The independent window updated 120 times. Adding the parent reduced that to 38, with the longest update interval rising from 25 to 147 ms. An empty AppKit parent-and-child control reproduced a similar slowdown without a WebView.</p><p>This located the problem in the native parent-and-child drag path on the tested Mac. Counts refer to observed position changes, not rendered frames. Maximum intervals exclude the initial wait after pressing. These are controlled observations from several runs, rather than cross-device benchmarks or repeated-run averages.</p><div class=\"journal-figures\"><figure class=\"journal-figure\"><a class=\"journal-figure-media\" href=\"https://bruce.flydog.ai/blog/media/tauri-window-dragging/measurements.en.svg\" target=\"_blank\" rel=\"noopener noreferrer\"><picture><img src=\"https://bruce.flydog.ai/blog/media/tauri-window-dragging/measurements.en.svg\" width=\"1000\" height=\"650\" alt=\"With 120 drag inputs: independent blank Tauri, 120 position updates and a 25 ms maximum gap; adding a native parent, 38 and 147 ms; Settings before, 23 and 256 ms; the first repaired Settings and Push runs, both 113 and 24 ms.\" loading=\"lazy\" decoding=\"async\"></picture></a><figcaption>Figure 1. Blank-window controls and the first repair validation. The parent relationship changed movement even before business content was added.</figcaption></figure></div></section><section id=\"native-parent\"><h2>A parent changes native window behavior</h2><p>Parenting is an official system mechanism. Apple&#39;s addChildWindow maintains relative ordering and makes the child move with its parent. Keeping Push above its project window was a concrete reason to use it.</p><p>Business ownership and native attachment are separate relationships. Tauri&#39;s parent interface establishes an owned window on Windows, a transient relationship on Linux and a native child window on macOS. One interface name covers different platform behaviors.</p><p>The experiment established that adding native parenting reproduced the dragging pauses in this Mac&#39;s environment. The internal system reason remained unresolved. The result does not establish that every macOS or Tauri window has the same problem.</p><ul class=\"article-sources\"><li><a href=\"https://developer.apple.com/documentation/appkit/nswindow/addchildwindow(_:ordered:)\" rel=\"noopener noreferrer\">Apple AppKit · addChildWindow</a></li><li><a href=\"https://docs.rs/tauri/2.12.1/x86_64-apple-darwin/tauri/webview/struct.WebviewWindowBuilder.html#method.parent\" rel=\"noopener noreferrer\">Tauri 2.12.1 · WebviewWindowBuilder.parent</a></li></ul></section><section id=\"drag-lifecycle\"><h2>Detach for the duration of the drag</h2><p>We applied a bounded workaround to the demonstrated behavior. At the native titlebar press, temporarily detach the child. After the actual button release, restore the relationship on the main thread. Business ownership, modal state and close management remain intact throughout.</p><p>One local measurement mattered: performWindowDragWithEvent returned almost immediately while the gesture was still in progress. Restoring the parent as soon as that call returned put the window straight back onto the slow path. We therefore read the left-button state through CoreGraphics and restore after release.</p><p>Before reattaching, we check that both windows still exist, are visible and retain the same business relationship. Validation also moves the parent after release to confirm that the child still follows it.</p><div class=\"journal-figures\"><figure class=\"journal-figure\"><a class=\"journal-figure-media\" href=\"https://bruce.flydog.ai/blog/media/tauri-window-dragging/drag-lifecycle.en.svg\" target=\"_blank\" rel=\"noopener noreferrer\"><picture><img src=\"https://bruce.flydog.ai/blog/media/tauri-window-dragging/drag-lifecycle.en.svg\" width=\"1000\" height=\"650\" alt=\"Before dragging, keep native parenting. On the titlebar press, detach and let the system drag. When the left button is released, verify window state on the main thread and reattach. Business ownership and modal state persist in all three stages.\" loading=\"lazy\" decoding=\"async\"></picture></a><figcaption>Figure 2. Native attachment is suspended for the gesture. Business ownership and modal state persist throughout.</figcaption></figure></div></section><section id=\"titlebar-regression\"><h2>A titlebar regression from the drag repair</h2><p>Another report followed: the child window&#39;s title was incomplete and overlapped the macOS traffic lights. This was a regression we had introduced. Native measurements found the text present, but positioned at x = -10 pt.</p><p>Disabling the new drag hit view restored the title. Adding that transparent view directly to NSThemeFrame had changed AppKit&#39;s titlebar hierarchy in the tested environment. We moved it inside the existing titlebar, preserving the system container and button layout.</p><p>The Settings title returned to x = 82 pt, beyond the traffic lights&#39; right edge at x = 69 pt. Both captures below show the same Appearance settings page. The article focuses on their titlebars; opening an image reveals the complete original capture.</p><div class=\"journal-figures journal-figures-pair\"><figure class=\"journal-figure journal-figure-titlebar\"><a class=\"journal-figure-media\" href=\"https://bruce.flydog.ai/blog/media/tauri-window-dragging/settings-title-before.png\" target=\"_blank\" rel=\"noopener noreferrer\"><picture><img src=\"https://bruce.flydog.ai/blog/media/tauri-window-dragging/settings-title-before.png\" width=\"1920\" height=\"1424\" alt=\"Actual Settings window before repair. Its title is pushed to the left edge and overlaps the macOS traffic lights.\" loading=\"lazy\" decoding=\"async\"></picture></a><figcaption>Figure 3. Before repair. The title existed, but native layout placed it against the left edge.</figcaption></figure><figure class=\"journal-figure journal-figure-titlebar\"><a class=\"journal-figure-media\" href=\"https://bruce.flydog.ai/blog/media/tauri-window-dragging/settings-title-after.png\" target=\"_blank\" rel=\"noopener noreferrer\"><picture><img src=\"https://bruce.flydog.ai/blog/media/tauri-window-dragging/settings-title-after.png\" width=\"1920\" height=\"1424\" alt=\"Actual Settings window after repair. The complete title sits to the right of the macOS traffic lights, with page content below the native titlebar.\" loading=\"lazy\" decoding=\"async\"></picture></a><figcaption>Figure 4. After repair. Preserving the system hierarchy restores spacing between the title and controls.</figcaption></figure></div></section><section id=\"platforms-and-validation\"><h2>Native captions and complete interaction checks</h2><p>Loaded dialogs, portal-drawn dialogs and separate panel windows now share caption configuration. macOS shows the native title with content below the strip. Windows and Linux retain system decorations. Drawn windows also set their native caption explicitly before showing, reducing reliance on title-change notifications from a blank document.</p><p>After the titlebar correction, Settings and Push each still produced 113 position updates. Maximum intervals were 27 and 35 ms; opening took 162 and 204 ms. An earlier full-window run recorded 239–248 ms opening. Individual observations vary and need to stay attached to their test conditions.</p><p>Settings, Push and Diff titles were inspected in real macOS windows. Windows and Linux configuration is covered, but visual behavior has not yet been checked on those systems. Passing tests, type checks and builds cannot replace that inspection.</p><ul><li>Position: center the child against its current parent and fit the display&#39;s usable area.</li><li>Drag: record continuous movement, final displacement and whether titlebar presses enter the page.</li><li>Restore: confirm parent movement carries the child after release, and Cancel clears the modal blocker.</li><li>Native appearance: inspect caption text, system controls and the content boundary together in the actual window.</li></ul></section><section id=\"next-investigation\"><h2>Start the next investigation with an empty window</h2><p>The largest change should be to the order of investigation. We trusted native dragging too early and treated better opening latency as evidence of a better overall window. The blank-window control should have come first.</p><p>Bruce&#39;s companion browser helps inspect DOM, business flows and requests. The real macOS window uses WKWebView, and its titlebar and native movement need measurements from that surface. Establish where the gesture lands, then choose the tool that reaches it.</p><p>Next time, we will begin with an independent empty window, add the parent relationship, then introduce the page, styles and business state, and finally check displays, themes and window ordering. Measure again at each step so every change answers a specific question.</p></section>",
      "date_published": "2026-11-10T00:00:00.000Z",
      "authors": [
        {
          "name": "Yang Yong'an and Codex"
        }
      ],
      "tags": [
        "engineering"
      ],
      "language": "en",
      "_bruce_event_date": "2026-11-10"
    },
    {
      "id": "https://bruce.flydog.ai/blog/good-job-bruce",
      "url": "https://bruce.flydog.ai/blog/good-job-bruce?lang=en",
      "title": "Good job, Bruce: a name, a companion, a puppy",
      "summary": "The names, the driving-dog joke and the trust behind Bruce. From early exploration involving Grok Bot to ice-blue C and native SVG, keeping the puppy’s story together.",
      "content_html": "<h1>Good job, Bruce: a name, a companion, a puppy</h1><p>The names, the driving-dog joke and the trust behind Bruce. From early exploration involving Grok Bot to ice-blue C and native SVG, keeping the puppy’s story together.</p><p>The Bruce team · Published <time datetime=\"2026-11-05\">2026-11-05</time></p><section id=\"names\"><h2>Free to fly. Quick to move. A companion beside you.</h2><p>In FlyDog, flight means freedom and speed. The dog is a loyal, approachable companion, ready to go with you. We want our tools to have that character too: quick to act, easy to work with, and there when you need them.</p><p>FlyDog is short, easy to say and easy to remember. We want the name to feel natural from the first introduction.</p><p>FlyDog is the parent brand. Bruce is the development companion that helps you turn ideas into something real. How the name FlyDog first came to us is a story for another day.</p></section><section id=\"bruce-c1\"><h2>Bruce took the manual-driving test. Or so the joke goes.</h2><p>Bruce brings Bruce Lee to mind for many people. Agility and real skill are a happy coincidence, and qualities we hope to earn. The name stayed with us because of a dog viewers call “Bruce” in Chinese dog-driving memes, and a running joke about its C1 manual-driving test. A dog sitting earnestly at the wheel, with a relaxed human beside it: that image stayed with us.</p><p>The driving-test claim is a joke. What we liked was the feeling of trust: someone takes care of the complicated controls, and you can finally breathe. Our founder kept a screenshot of the clip as it circulated. We wanted to keep the thought behind it, too.</p><ul class=\"article-sources\"><li><a href=\"https://www.bilibili.com/video/BV1nN411G7V9/\" rel=\"noopener noreferrer\">A Chinese post using the driving-test joke · 2023</a></li><li><a href=\"https://www.bilibili.com/video/BV1wD4y1g7rF/\" rel=\"noopener noreferrer\">A “You’ve got it, Bruce!” video post · 2023</a></li><li><a href=\"https://www.sina.cn/news/detail/5254032233070765.html\" rel=\"noopener noreferrer\">“Good job, Bruce” used for an adventure companion · 2026</a></li></ul></section><section id=\"good-boy-bruce\"><h2>From setting out to getting it done.</h2><p>Before we begin: “You’ve got it, Bruce!” After the job is done: “Good job, Bruce!” Encouragement before setting out, celebration after finishing. Simple words, with the comfort of trusting a companion. Similar expressions appear on clips of dogs at motorcycle handlebars or out on adventures. The footage varies; the feeling is easy to understand.</p><p>We imagine a brave puppy that moves forward and welcomes difficult challenges. We hope Bruce can take on more of development’s manual controls: installing agents, configuring environments, coordinating sessions, launching scripts, and the small chores that pull you away from an idea. You choose the direction. The tools should gradually handle more of the fiddly work of getting there, making the experience feel more like driving an automatic.</p><p>That is where we want to go. Each small task made easier brings us closer to a companion you can rely on. And another reason to say: Good job, Bruce.</p></section><section id=\"first-drafts\"><h2>The puppy came before today’s SVG.</h2><p>Grok Bot contributed to the early brand and icon exploration in September. Tools including Cursor also helped implement the artwork and integrate it into the apps. We went back and forth between V, play, and code-bracket ideas before keeping the puppy in profile, wearing goggles. Choosing an image, redrawing it, and putting it into an app were distinct jobs.</p><p>These eight recovered studies bring that early work back into view. Some directions were briefly used; others remained alternatives in the archive. The numbers make them easier to browse, rather than setting out a strict order of creation.</p><p>The later ice-blue C addressed a problem with the existing puppy: it needed to stand out at small sizes. Codex then rebuilt the selected image as a native SVG. That vector refinement continues the earlier work, and gives us another reason to keep the first attempts alongside it.</p><ul><li>6 September 2026: V, play, and code brackets were compared and revisited within the same day.</li><li>9 September 2026: the goggled profile entered the apps. Its silhouette connections were revised, and we selected a more detailed puppy image.</li><li>12 September 2026: the approved character was preserved while the area outside the rounded plate was cleaned up.</li><li>5 November 2026: we selected ice-blue C. Codex subsequently completed this round of native SVG reconstruction.</li></ul></section><section id=\"explore\"><h2>Start by making the possibilities visible</h2><p>This refinement round began with an open question: could Bruce use a new symbol inspired by FlyDog&#39;s geometric language? We explored a folded B, twin wings, a negative-space B and a pilot cue side by side. Each offered a direction, but none became the final mark.</p><p>The useful result was seeing the existing identity more clearly. Bruce&#39;s puppy already had an expression: a left-facing profile, two ears, a tilted head, goggles and a violet badge. Together they created familiarity. A complete-looking new symbol did not by itself justify giving up those recognition cues.</p></section><section id=\"keep\"><h2>Return to the familiar puppy</h2><p>The feedback clarified the brief: use the existing puppy as the reference and improve it from there. We could keep the character we liked while addressing the difficulty of seeing it clearly in use.</p><p>Its dark fur sat close to the original plate, and details merged when reduced. The task narrowed from establishing a new identity to separating foreground and background, adjusting the face&#39;s presence and improving small-size recognition. Ears, goggles, expression and badge became constraints to preserve rather than decorations to replace.</p></section><section id=\"choose\"><h2>Measure A, B and C on the same scale</h2><p>We compared large artwork with actual 16, 24, 32 and 48px use cases, on both light and dark interface backgrounds. Detail in a large image cannot stand in for clarity in a small icon. The three studies tested continuity, a different plate color and stronger foreground/background separation.</p><p>C became the final choice. A pale ice plate lifted the dark outline and gave the face, goggles and badge clearer layers. This was a decision based on a practical use problem. The other originals remain evidence of exploration rather than alternative official identities.</p><ul><li>A / Teal continuity: keep the original teal character, strengthen the outline and enlarge the face. Familiar, but the dark fur still sits close to its plate.</li><li>B / Indigo exploration: a more vivid indigo-and-cyan combination, while structure inside the dark plate still relies on bright edges.</li><li>C / Ice separation: distinguish the pale plate from dark structural lines so the familiar puppy remains easier to recognize at small sizes.</li></ul></section><section id=\"vector\"><h2>Make every line a real curve</h2><p>After choosing C, the next requirement concerned the file itself: do not simply put the picture inside an SVG. Wrapping a PNG does not create editable shapes that remain vector geometry when enlarged.</p><p>We fitted the original contours and color regions in layers, preserving the face, ears, goggles, collar and chevrons while removing edge speckles and tiny surface artifacts. The full mark consists of eighteen editable paths and six native gradients, is about 14KB, and embeds no bitmap.</p><p>The plate uses a consistent ice gradient and rounded outline. Websites, shared interface components and vector-capable platforms use the native SVG. Required PNG, ICO and ICNS resources are exported from that master with each platform&#39;s safe areas, transparency and mask requirements. Identity changes have one clear source.</p></section><section id=\"sizes\"><h2>Put the full mark and sidebar glyph into their actual sizes</h2><p>The full mark is checked at 16, 24, 32, 48 and 64px on light and dark backgrounds. The value of vector art includes preserving structure at its target size. The illustrated article also offers a sliding overlay of the original and vector on the same canvas and at the same scale.</p><p>The Agent sidebar needs a different density. The first monochrome companion extracted the ears, goggles and friendly profile for a 20px normal stripe and 16px compact mode. In the actual interface, though, its gaze felt blank, the neck disappeared, and the strap crowded the outline.</p><p>Feedback on 6 November clarified the goal: simplify the drawing while keeping the puppy&#39;s expression. We adjusted the balance between head and neck, made room for the collar, and used a connected brow and eye with an upturned smile to keep Bruce alert and friendly. Optical gaps around the goggles and strap reduced crowded joins.</p><p>We compared the refined glyph at 16, 20 and 24px on light and dark backgrounds and inspected the real Tauri window&#39;s 20px sidebar. A single color follows the interface state. The illustrated story keeps a before-and-after comparison, and the download archive includes both the current glyph and the first version labeled as historical material.</p><p>The companion glyph and full-color mark have different presentation jobs but identify the same Bruce. The shared component and website workbench demo use the same geometry, keeping one concept from acquiring different symbols at each entry point.</p></section><section id=\"materials\"><h2>Keep the result together with the reasoning</h2><p>The official full mark is bruce-mark.svg; its Agent companion is bruce-agent.svg. The design archive also retains four early symbols, the previous puppy, original A/B/C images, large and small comparisons, design notes and a file manifest. The originals are historical evidence; current identity resources follow the SVG master.</p><p>Keeping the process explains why the puppy stayed, why we chose a light plate and why the sidebar needed its own glyph. Developers and their agents can follow the problem, constraints, comparisons and decisions, while media introductions have a complete account to draw on.</p><p>FlyDog is the parent brand and Bruce its product. We keep a familiar character while making its identity clearer across the website, applications and toolbars.</p></section>",
      "date_published": "2026-11-05T00:00:00.000Z",
      "authors": [
        {
          "name": "The Bruce team"
        }
      ],
      "tags": [
        "design"
      ],
      "language": "en"
    },
    {
      "id": "https://bruce.flydog.ai/blog/distance-not-time",
      "url": "https://bruce.flydog.ai/blog/distance-not-time?lang=en",
      "title": "Distance, not time: investigating terminal scrolling",
      "summary": "Seven rounds of measurements could not explain a heavy scroll. The turning point was measuring how far the view moved for the same gesture.",
      "content_html": "<h1>Distance, not time: investigating terminal scrolling</h1><p>Seven rounds of measurements could not explain a heavy scroll. The turning point was measuring how far the view moved for the same gesture.</p><p>Yang Yong’an + Claude Code · Published <time datetime=\"2026-11-05\">2026-11-05</time> · Originally recorded <time datetime=\"2026-09-21\">2026-09-21</time></p><section id=\"the-feeling\"><h2>“It does not follow my hand” is not yet a diagnosis</h2><p>The original report was specific: a full-screen TUI inside Bruce scrolled smoothly under inertia, yet felt heavy during repeated changes of direction. The reference was a familiar IDE terminal. We interpreted the problem as slow response and started measuring latency.</p><p>This article adapts a development record from September 2026. Its numbers belong to the device, versions and fixtures used then; they are not performance promises for every terminal or computer. The useful part is the method for turning an accurate physical description into measurements that distinguish possible causes.</p></section><section id=\"good-numbers\"><h2>Healthy numbers, the wrong explanation</h2><p>We checked writing, transport, program responses, parsing, frame timing and the glyph atlas. Some improvements were real: shorter batching windows, better scheduling and preventing a blocked write from holding up other work. None explained the consistent heavy feeling.</p><p>The fixture mattered too. A minimal browser page, an empty newly started TUI and the native window someone is using are different scenes. We added a development evaluation channel into the actual window, so a request could inspect live state and return a result. Only then could we identify the path taken by a real gesture.</p><p>One trace showed the terminal moving its own scrollback. The gesture never entered the path we had been optimizing, which sends wheel reports to the program. Establishing who owns the gesture should come before optimizing its handling.</p></section><section id=\"travel\"><h2>The turning point was a simpler comparison</h2><p>The developer repeated the gesture in another terminal and described the difference more precisely: it was not taking too long; it was not traveling far enough. Time and distance finally became separate questions. A responsive system can still feel sluggish when its displacement gain is wrong.</p><p>A recorded three-second gesture contained 165 events totaling roughly 1002px, averaging about 6.1px per event. The relevant path converted pixels into rows and multiplied events below 50px by an additional 0.3. A trackpad split the gesture into many small steps, nearly all of which were reduced. At the measured row height of about 15px, roughly 66 reports were expected, but only about 20 arrived.</p><p>This affected wheel reports sent to the full-screen application, not every kind of scrolling. When two paths are mixed together, healthy latency measurements cannot establish whether the distance is correct.</p></section><section id=\"fix-the-path\"><h2>Apply the correction to the path that needs it</h2><p>Our first compensation targeted the wrong path. It amplified already healthy scrollback and produced overshoot. The corrected condition was narrower: the event must go to the program, use pixel mode, and fall within the original range affected by the reduction. This snippet preserves only the core decision used at the time.</p><p>This is not a global sensitivity increase. Larger mouse steps did not receive the same reduction and must not be amplified with them. We also turned equal total travel with different event partitions into regression cases and submitted the issue and a proposed fix upstream at the time. Local behavior still needed its own verification.</p><pre><code class=\"language-typescript\">const sensitivity = toProgram &amp;&amp; pixelMode &amp;&amp; Math.abs(deltaY) &lt; 50\n  ? configured / 0.3\n  : configured;</code></pre></section><section id=\"takeaways\"><h2>Leave the next investigation a shorter route</h2><p>The durable result was more than a scroll correction. Access to the actual window, instruments that report their own failures and a habit of comparing the same gesture can help the next developer or Agent avoid applying correct numbers to the wrong scene.</p><ul><li>Identify the surface and processing path that own the event.</li><li>Check both elapsed time and displacement when something feels unresponsive.</li><li>Record the device, version, content and foreground state with the measurement.</li><li>Turn the decisive comparison into a regression case, rather than leaving only a feeling of improvement.</li></ul></section>",
      "date_published": "2026-11-05T00:00:00.000Z",
      "authors": [
        {
          "name": "Yang Yong’an + Claude Code"
        }
      ],
      "tags": [
        "interaction"
      ],
      "language": "en",
      "_bruce_event_date": "2026-09-21"
    },
    {
      "id": "https://bruce.flydog.ai/blog/the-missing-release",
      "url": "https://bruce.flydog.ai/blog/the-missing-release?lang=en",
      "title": "Where did the mouse-up go? Investigating a missing click",
      "summary": "A light tap failed, while a firm press restored it. Following the event chain revealed a missing release before the page, and a recovery policy that shaped everything after it.",
      "content_html": "<h1>Where did the mouse-up go? Investigating a missing click</h1><p>A light tap failed, while a firm press restored it. Following the event chain revealed a missing release before the page, and a recovery policy that shaped everything after it.</p><p>Yang Yong’an + Claude Code · Published <time datetime=\"2026-11-05\">2026-11-05</time> · Originally recorded <time datetime=\"2026-10-02\">2026-10-02</time></p><section id=\"an-incomplete-click\"><h2>A click with one half missing</h2><p>The report sounded like a frontend issue: some settings tabs ignored a light trackpad tap, yet a firm press made subsequent taps work again. We had previously blamed similar behavior on React click compatibility and moved some actions to the press. That explanation had never been measured all the way through.</p><p>This time we recorded events in the actual window. A failure contained pointerdown and mousedown, then no release. The next action lacked a fresh pointerdown. WebKit still believed the previous button press was held. The question changed from an unreliable click to a missing release somewhere in the pipeline.</p><pre><code class=\"language-text\">pointerdown → mousedown → …\nNext action: mousedown → pointerup → mouseup → click</code></pre></section><section id=\"follow-the-event\"><h2>Ask each layer whether it received the event</h2><p>First we confirmed that hit testing reached the intended button, without an overlay or drag region intercepting it. We then checked the WebView, window, application and events delivered to the process. The missing mouseUp appeared in none of them. Even the system continued reporting the left button as pressed. Changing the page handler could no longer explain this evidence.</p><p>The test machine ran Magnet, a window-management utility with an active event interceptor watching mouse presses and releases. Quitting it changed the comparison: two recorded groups of eight clicks lost two to three releases; twelve clicks of valid duration after quitting it lost none. This was a result for the device and utility state tested then, not a finding about every installation.</p><p>Synthetic input needed its own control. A 0ms press-and-release could fail in a minimal native window too, so it could not stand in for a real tap. An instrument defect nearly became our product diagnosis.</p></section><section id=\"recovery\"><h2>The same missing event can produce different experiences</h2><p>We compared the same class of events in Chromium and WKWebView. Both could receive incomplete sequences; their behavior after the loss differed. In the measurement, Chromium ended a stale press before accepting a new one, while WebKit could continue with the incorrect state.</p><p>Title-bar ownership mattered as well. A native title-bar click need not enter the webpage, whereas Bruce&#39;s web-based drag region involves the WebView. A report that Electron feels better may involve recovery and event boundaries; it does not establish that one framework&#39;s click handler is more reliable.</p></section><section id=\"repair-with-boundaries\"><h2>Repair state without inventing intent</h2><p>Bruce cannot repair another application&#39;s event interceptor, but it can maintain press/release pairing at its own WebView entry points. The implementation combines system button-change signals, the event queue and a subsequent real press to decide whether a repair is needed.</p><p>Location matters as much as timing. An early repair with a trustworthy target can finish the tap at its original press location. A late repair resets state without carrying out an action the person has abandoned. Releasing at the pointer&#39;s new location can instead give a common ancestor a ghost click.</p><p>Follow-up work distinguished pointer drags from native drag sessions and tracked late real releases to avoid settling one action twice. Repairing a missing event should not mean synthesizing clicks indiscriminately across targets.</p></section><section id=\"make-the-evidence-useful\"><h2>Leave a route the next developer can follow</h2><p>These notes can help developers working with WebViews, native windows or hybrid interfaces, as well as Agents investigating on their behalf. A specific repair may change with platform versions. Observability and controlled comparisons remain useful.</p><ul><li>Separate hit testing, press, release, dispatch and state recovery.</li><li>Check whether each layer received the event, and stop blaming boundaries already ruled out.</li><li>Keep failing and successful controls, with the synthetic-input conditions recorded.</li><li>Confirm the target window before an instrument posts input.</li><li>Reset stale state without completing an intent the person has already abandoned.</li></ul></section>",
      "date_published": "2026-11-05T00:00:00.000Z",
      "authors": [
        {
          "name": "Yang Yong’an + Claude Code"
        }
      ],
      "tags": [
        "interaction"
      ],
      "language": "en",
      "_bruce_event_date": "2026-10-02"
    },
    {
      "id": "https://bruce.flydog.ai/blog/one-store-one-agent",
      "url": "https://bruce.flydog.ai/blog/one-store-one-agent?lang=en",
      "title": "Why one data store has one Agent",
      "summary": "A session appeared on the desktop but not on the phone. Two parallel services led us back to ownership, discovery and explicit client context.",
      "content_html": "<h1>Why one data store has one Agent</h1><p>A session appeared on the desktop but not on the phone. Two parallel services led us back to ownership, discovery and explicit client context.</p><p>Bruce · Published <time datetime=\"2026-11-05\">2026-11-05</time> · Originally recorded <time datetime=\"2026-10-28\">2026-10-28</time></p><section id=\"two-different-answers\"><h2>Two entry points that each looked healthy</h2><p>During one investigation, the resident Agent and a development window&#39;s Agent served the same data directory in parallel. The window searched a presumed port, found nothing there and started another service. The phone&#39;s entry point belonged to the service that had arrived first. A new desktop session never appeared on the phone, with no obvious error on either side.</p><p>Adding another refresh could not explain the situation. The entry points had found different service instances. The October 2026 fix started with a more basic rule: ownership of shared state must be explicit.</p></section><section id=\"store-identity\"><h2>The boundary is a data store, not the entire device</h2><p>Bruce now uses the data directory as the ownership boundary. One Agent holds a store. A second launch for that store discovers the existing holder, says who is serving and steps aside. Clients no longer need to guess a fixed port independently.</p><p>Separate stores may have separate Agents. This is useful isolation, particularly for tests, which must not mix their fixtures with a person&#39;s task list. A rule allowing only one process on an entire computer would erase that useful distinction.</p></section><section id=\"lock-and-record\"><h2>A lock establishes ownership; a record enables discovery</h2><p>The implementation separates two jobs. An operating-system file lock establishes who may serve the state, and the Agent acquires it before touching the store. The lock is released when its holder exits. A file remaining on disk must not be mistaken for a lock still being held.</p><p>After claiming ownership, the Agent records its process, listening address and startup information. It writes the complete record to a temporary file before renaming it, so readers do not see half a record. Other entry points read that record to locate the serving instance.</p><p>A record can outlive its process, so finding it does not establish that the service is online. The caller must still check that the recorded address answers. Keeping ownership, discovery and availability distinct makes failures easier to locate.</p><pre><code class=\"language-text\">One data store\n  Lock     → Who may serve this state\n  Record   → Where to find the holder\n  Response → Whether it is available now</code></pre></section><section id=\"context\"><h2>A shared service still needs explicit project context</h2><p>Finding the service is only the first step. A window, phone or other client must identify the project, session and requester. The service cannot infer which directory a phone means from the most recently active window.</p><p>This separates the interface from the work state. Clients express their current context; the service owns the tasks, terminals and records it holds. Changing entry points should not accidentally switch to a different body of work. Access remains governed by the relevant permissions and connection configuration.</p></section><section id=\"regression-and-lifetime\"><h2>Test the boundary and explain the lifetime</h2><p>Regression tests cover a second claim on the same store being rejected, ownership becoming available after release, and independent stores remaining independent. Those conditions are more precise than one successful-looking connection.</p><p>Closing a client and restarting the Agent are different operations. A store lock and discovery record do not keep terminal processes alive across an Agent restart. Continued work still depends on an online host, service settings and the actual process lifetime. Lossless handoff requires separate design and verification.</p><ul><li>Confirm that both clients mean the same data and service before investigating synchronization.</li><li>Use explicit ownership to prevent launch races, and discovery records instead of port guesses.</li><li>Declare the project, session and requester rather than inferring them from recent interface activity.</li><li>Give tests isolated storage and preserve the person&#39;s working sessions.</li></ul></section>",
      "date_published": "2026-11-05T00:00:00.000Z",
      "authors": [
        {
          "name": "Bruce"
        }
      ],
      "tags": [
        "engineering"
      ],
      "language": "en",
      "_bruce_event_date": "2026-10-28"
    }
  ]
}
