Ask Logue part 6: the island as glass, with starters, a level meter and a Return hint - #75
Ask Logue part 6: the island as glass, with starters, a level meter and a Return hint#75shanforge wants to merge 13 commits into
Conversation
The prompt pill was filled with a fixed near-opaque dark colour and the transcript above it used .ultraThinMaterial, so one island rendered in two materials with the seam between them visible whenever both were on screen. Worse, nothing pinned the island's colour scheme. Every foreground in it is white, and Material is not: in Light appearance .ultraThinMaterial is a light frost, so the transcript drew white text on a white-ish panel. IslandSurface is the rule, free of SwiftUI so the matrix is testable without mounting a view. Reduce Transparency stops the island being glass at all rather than making it slightly less glass, and takes the shadow with it; Increase Contrast may only ever deepen the veil and harden the edge, never lighten either; and the scrim can never fall below a floor, because glass with no veil over a white document is white text on a white panel. Parked on its own branch: this is #62 (part 2), not part 1. Part of #62.
Both panels now paint through IslandSurface, so the island stops rendering in two materials — the prompt pill was filled with a fixed near-opaque colour while the transcript directly above it was frosted, and the seam showed whenever both were up. The legibility bug underneath it: nothing pinned the island's colour scheme. Every foreground in it is white and Material is not, so in Light appearance .ultraThinMaterial resolved to a light frost and the transcript drew white text on a white-ish panel. The island now commits to dark rather than half its palette adapting — it is a HUD over someone else's window, not a document — which is what makes it legible whichever appearance the system is in. Reduce Transparency and Increase Contrast are honoured for the first time here; the rule and its cases came with the previous commit. Part of #62.
Which chips appear was already a tested rule in HomeSuggestions; how the workspace is read into that rule was three private computed properties inside AgentChatView. That is why the island has no starters — the rule was reachable and the inputs were not. Copying them would have been the wrong version: workspaceIsEmpty alone spans three stores, and a second copy that forgot spaces offers a returning user the first-run chips. The stores-loaded guard has the same shape of bug behind it. Part of #62.
Three things the island never said, all of them about the moment before a send. Starters are the chips Home already offers, from the same rule and the same reading of the workspace — so an empty island suggests summarising the meeting you have not summarised rather than a hardcoded list that goes stale. Held back until the stores report, because offering first-run chips to a returning user is worse than offering nothing. The level meter is the indicator the two in-app chat panels already mount. The island streams the partial transcript straight into the field so it can be edited before sending, so the indicator is handed none — what it adds is the level, which is the only thing that says the mic is hearing anything, and a stop target bigger than the mic glyph. The Return hint appears only once there is something to send. A hint that is always there is chrome, and the island has one line to spend. Part of #62.
Two defects in formatArguments that only became visible at island width, where the card is 700pt rather than a full window and the row has to hold the summary, the status and the Approve and Deny buttons at once. It mapped over a Dictionary, which has no order, so the same call rendered `query: standup, limit: 5` on one pass and `limit: 5, query: standup` on the next — and with the line truncated, the argument you could actually read changed as the view re-rendered. Keys are sorted now: alphabetical is arbitrary, but it is the same arbitrary every time. And it was unbounded. update_document carries the whole new body as an argument, so the string handed to Text was the length of a document — 20,009 characters in the test that now guards it. lineLimit(1) hid the tail but the text was still laid out, pushing everything after it on the row for room that was never going to be used. Values are also flattened onto one line, so a multi-paragraph argument stops looking like a short one. The approval buttons no longer compress: how to refuse must never be what gives way, and the summary beside them is already truncating. Part of #62.
Every destructive tool takes a UUID. The card had five hand-written sentences and a fallback of "Agent wants to run <toolName>", and none of them named the target — so "Agent wants to delete a document" was the whole of what the user was told before being asked for Touch ID. Which document was not knowable from the card at all. ToolApprovalPrompt is the wording and which argument holds the target, pure so both are testable; ToolApprovalTargetResolver is the half that has to ask the stores. Reminders and calendar events deliberately resolve to nothing: both live behind an EventKit permission, and prompting for access as a side effect of drawing an approval card is not acceptable in the one card whose job is to be trustworthy. They show the action alone, which is honest, rather than a UUID, which is not. The coverage test walks the real registry, so a destructive tool with no sentence is a red build. It found two on its first run — fetch_contacts and get_location, both reading private data, both previously rendering as "Agent wants to run get_location". AgentCoordinator gained allKnownTools() so that walk cannot go stale; the registry is built from it rather than from a second list. The approval sentence also renders as primary text over two lines. A title cut in half is how you approve the wrong thing; everything else on a card is a record of something that already happened and stays on one line. Part of #62.
The island had nothing there. Between pressing Return and the first token it showed the user's own bubble and empty space — which is exactly when someone concludes the send did not land and presses Return again. Once the assistant message existed it showed a literal "..." pushed through the markdown renderer, in a grey bubble. It now shows the pulsing row the main window shows, saying what the agent is actually doing rather than a static "Thinking…". The wording already had one definition in UICopy.Status.describe. When to show it did not — it was written out longhand at each place that needed it, and came out differently at each. AgentThinkingState is that half, and both surfaces read it. The input worth naming is pendingAnswerText: what has arrived of the answer being produced *now*, not the last assistant message. The latter is the previous answer and is non-empty for the whole of every later gap, so an indicator keyed on it appears once and never again. There is a case pinning that. Part of #62.
The island floated its chips in an overlay offset 34pt above the pill. That meant two things: they took no layout space, so with a conversation on screen they drew over the bottom of the transcript; and nothing bounded them, so attaching six files ran the row past the island's width and off both ends. The row is now in the VStack, and ComposerChipRow decides what fits. The ordering rule is the part worth stating: modes are never hidden. A mode chip says what the send is about to do — search the web, spend minutes on Deep Research — and a hidden one is a send the user did not know they were making. Attachments overflow into a counter instead, which loses nothing, because the files are still attached. The counter carries the names it hides, as a tooltip and as its VoiceOver label. A chip that hides a file with no way to find out which is worse than a row that overflows. Modes are a value now rather than two booleans added up wherever a count was needed. Part of #62.
Almost every control in the island carried only .help(…). On macOS that becomes the accessibility hint, not the label — so VoiceOver fell back to the only other thing it had, the SF Symbol name. The send button announced "arrow up"; the close button announced "xmark"; and the two mode toggles announced nothing about being on, which is the single thing a toggle exists to convey. IslandControlCopy holds the name, the state and the tooltip together so they cannot drift, and .islandControl applies all three at once — writing them out separately at each call site is exactly how one gets forgotten, which is how this happened. Send and Stop are named differently rather than being one control in two states. A button that keeps the name "Send" while it cancels is how someone stops a run they meant to let finish. The test walks every control in every state and rejects a dotted, lowercase label. Restoring "arrow.up" on the send button turns it red. Part of #62.
The island is the most animated surface in Logue — it slides in over another app, springs open on the first message, scales its chips in and out, and runs two forever pulses. None of it consulted Accessibility → Display → Reduce motion, so someone who asked the system for less movement got the most of it here. IslandMotion is the policy, and it turns on decoration versus information. A spring, a slide and a scale say nothing the layout does not already say, so they go — replaced by a cross-fade rather than by nothing, since removing the transition outright turns every panel into a jump cut. A pulsing dot is the harder case: it is the only thing on screen saying the agent is still working. It stops anyway, but only because every row it appears in also says so in words. The level meter is deliberately untouched — its movement is the reading, and a still meter is not a calmer meter, it is a broken one. Springs are removed rather than shortened. A spring is the movement; making it quick makes it a flinch. PulsingDot reads the setting itself rather than at each call site, so a dot added somewhere new cannot reintroduce a forever-repeating animation. Part of #62.
#74 took `shan/issue-61-island-tools-and-research` to `main` carrying review-round fixes this branch was cut before, so the two had diverged in three files. Three resolutions worth stating, because in each case one side is not simply newer: - **The three composer glyphs are gone, not relabelled.** This branch put VoiceOver labels on the island's separate attach / web-search / Deep Research buttons; `main` had since replaced all three with the shared `ComposerPlusMenu`, which is the thing #61 asked for and already names itself for VoiceOver. So `IslandControlCopy.attach`, `.webSearch` and `.deepResearch` are deleted rather than kept — copy naming controls that no longer exist is copy no test can hold to account, and `IslandControlCopyTests` walked exactly those. A note in the enum says where they went. - **The one-shot flags keep `main`'s island-specific keys.** This branch read `oneShotWebSearch`; `main` had moved the island onto `islandOneShot*` so a send here cannot disarm a chip armed in the main window. The chip row keeps its `ComposerChipRow` bounding and reads the corrected keys. - **The layout animation keeps `main`'s `hasThread`, not this branch's `hasContent`.** `hasThread` is `hasContent || conversationID != nil`, from the fix that stopped an island holding a conversation closing itself. Keying the animation on `hasContent` would have quietly reverted the question that fix exists to ask. `stagedChips` also absorbs two things `main` fixed in the copy it replaces: the attachment's own `iconName` rather than a hardcoded "doc", and `UICopy.Input` titles. Verified on the merge result: build succeeds, 1656 tests in 145 suites pass, SwiftFormat 0.62.1 --lint clean over 543 files, SwiftLint 0.65.0 --strict 0 violations in 687 files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M3Wpnj9ZmWPKYdFPB1AVY3
Four findings, most severe first. **A refused send showed nothing (blocking).** `localError` was set on both refusal paths and rendered by none: the banner read `currentError`, which is `coordinator.lastError(in: conversationID)`. The globally-busy guard in `sendMessage` returns *before* `ensureConversation()`, so on that path `conversationID` is still nil and `currentError` can only ever answer nil. Start a run in the main window, open the island, type, press Return: the guard fires, sets the message, and nothing appears — which is verbatim the failure its own comment says it exists to prevent. The banner now reads `displayedError`, local first, and a send that goes ahead clears the last refusal so a stale "busy elsewhere" cannot sit over a send that worked. **A document title could reverse the sentence naming it (major).** `flatten` collapsed whitespace but let control and format characters through. A title carrying U+202E — which can arrive in a `.md` file dropped into the markdown folder, or from a `create_document` call a prompt-injected model made — reverses the display of everything after it, so `Delete “report.txt”` can be made to name a different file on the one card whose whole job is to be true, directly above a Touch ID prompt. `CLAUDE.md` already requires stripping control characters from any user string reaching a prompt or label, and `AgentArgs.sanitize` does it in eight other places. `ToolApprovalPrompt` and `ToolArgumentSummary` had a copy each of `flatten` and `clamp`, so the fix is a shared `DisplayText` rather than a third copy — a control that exists twice is a control that gets fixed once. The order inside it is the part worth stating: **a newline is itself a control character** (U+000A is Cc), so stripping controls before splitting on whitespace turned `first\nsecond` into `firstsecond` — a different value, silently. The existing suite caught exactly that. Whitespace is exempted from the strip and the split handles it. Mutation-checked: removing the strip turns six cases red across both files. **Two definitions of "the workspace is empty" (minor).** The branch extracted `storesAreLoaded` and the emptiness rule into `HomeSuggestions` so the island could share them, and left `AgentChatView`'s copies in use — each commented as being the one definition. Change the rule in one and `HomeLandingView` says empty while the chips say otherwise: first-run landing above returning-user chips, the drift both comments warn of. The view now reads the extracted rule. No runtime change — `LogueApp` injects the same singletons the extraction reads. **Reduce Motion was read once (minor).** `PulsingDot` consulted it in `onAppear` only, so turning it on while the island was streaming left the forever-repeating animation running until the view was rebuilt — the moment the setting is most likely to be reached for. Now also on change, and the scale resets rather than freezing mid-pulse. Verified: build succeeds, 1670 tests in 146 suites pass, SwiftFormat --lint clean over 544 files, SwiftLint --strict 0 violations in 689 files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M3Wpnj9ZmWPKYdFPB1AVY3
…'s fixes The second round reviewed the first round's changes, which is where the new defects were. **`workspaceIsEmpty` asked for work it then threw away.** The fix routed it through `HomeSuggestions.currentInputs(overdueCount: 0)` — a dummy argument, passed to reach a function that also finds the most recent unsummarized meeting and counts today's, none of which a boolean needs. `AgentChatView` then called `currentInputs` twice per render, once here and once for the chips. It now reads it once as `suggestionInputs` and derives both from it, which is also what makes the empty state and the chips provably agree rather than merely currently agree. **Three `@Environment` stores were left dead by that fix.** `meetingStore`, `documentStore` and `spaceStore` had no remaining reader in `AgentChatView` or its extensions. Removed — and the comment above them, which said the view read from the environment "rather than reaching for the shared singletons", had become the opposite of what the code does. It now says which two are still injected and why, and where the other three went. **`localError` was marked extension-visible and is not.** No file but `CommandCenterChatView` mentions it, so the `// Extension-visible: +Composer` marker named an extension that does not use it — the marker is how a reader knows the wider access is deliberate, so a wrong one is worse than none. Now `private`. Refuted this round, recorded so they are not re-examined from scratch: the `minimumScrimOpacity` floor is unenforced in `IslandSurface` but *is* swept by `IslandSurfaceTests` across the whole matrix, which is this codebase's stated way of holding a pure rule to account; and `IslandMotion.allowsPulse` stopping every pulse looked like it could remove the signal rather than the decoration, but all five `PulsingDot` call sites pair it with words, so the claim its doc makes holds today. Verified: build succeeds, 1670 tests in 146 suites pass, SwiftFormat --lint clean over 544 files, SwiftLint --strict 0 violations in 689 files. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M3Wpnj9ZmWPKYdFPB1AVY3
Rebased onto
|
Carries the two review rounds on #75 — the island's error banner fix, the shared `DisplayText` control-character strip, and the duplicate-definition cleanup. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01M3Wpnj9ZmWPKYdFPB1AVY3
Part of #56. Closes #62 — all eight boxes.
Stacked on #74 — review that first; this branch contains it.
The seam
The island rendered in two materials. The prompt pill was filled with a fixed
near-opaque colour and the transcript directly above it used
.ultraThinMaterial, sowhenever both were on screen the join between them was visible: one panel looked like
glass and the other like a rectangle stuck to the screen.
The legibility bug underneath it
Nothing pinned the island's colour scheme. Every foreground in it is white — bubbles,
placeholder, icons, chips — and
Materialis not. In Light appearance.ultraThinMaterialresolves to a light frost, so the transcript drew white text on awhite-ish panel.
Color.primary.opacity(0.06), used for the assistant bubble and theheader buttons, inverted with it.
The island now commits to dark rather than having half its palette adapt. It is a HUD
over someone else's window, not a document, so it does not follow the system — which
is what makes it legible in both appearances, and what makes every white already in
it correct by construction.
The rule is decided, not picked
The island is the only surface in Logue with no control over what is behind it — a
white document, a photograph, a video at full brightness.
IslandSurfaceis thatdecision, free of SwiftUI so the matrix is testable without mounting a panel:
slightly less glass, and takes the shadow with it — a soft shadow under an opaque
panel is the effect that setting exists to remove.
comparison rather than as numbers, so retuning the palette cannot quietly invert it.
is white text on a white panel — the failure the type exists to prevent.
boundary and reads as a hole cut in the screen.
Neither accessibility setting was honoured by the island before this.
Tests
7 cases in
IslandSurfaceTests, none needing a model. Mutation-checked: dropping thescrim below the floor turns "Every treatment keeps the scrim above the floor" red for
every combination.
Worth pinning because all of it is invisible in the one configuration a developer
actually runs — transparency on, contrast standard, a dark wallpaper. The settings
that break it are ones you have to go and turn on.
Verification
xcodebuild build— succeeds./scripts/test-no-llm.sh— 1623 tests in 142 suites pass--lint— 0/541 files require formatting--strict— 0 violations in 682 filesDraft until it has been seen
This one is entirely visual and I cannot drive the island — it needs Accessibility
permission, which every rebuild revokes. What to look at, over a bright wallpaper
and then a dark one:
readable. This is the case that was broken.
desktop no longer moves under it, and the drop shadow is gone.
hardens. Nothing gets lighter.
Box 2 — starters, the level meter, and the Return hint
Three things the island never said, all about the moment before a send.
Starters on an empty island
The chips Home already offers, from the same rule (
HomeSuggestions) and the samereading of the workspace — so an empty island suggests summarising the meeting you
have not summarised rather than a hardcoded list that goes stale.
Which chips appear was already a tested rule; how the workspace is read into it was
three private computed properties inside
AgentChatView, which is why the island hadno starters. Copying them would have been the wrong version —
workspaceIsEmptyalonespans three stores, and a copy that forgot spaces offers a returning user the first-run
chips. The stores-loaded guard has the same shape of bug behind it, so it moved too:
nothing renders until all three stores report.
A live level meter while the mic is open
The
VoiceInputIndicatorthe two in-app chat panels already mount. The island streamsthe partial transcript straight into the field so it can be edited before sending, so
the indicator is handed none — what it adds here is the level, which is the only thing
that says the mic is actually hearing anything, and a stop target bigger than the mic
glyph.
What Return will do, once it will do anything
Shown only when there is something to send. A hint that is always there is chrome, and
the island has one line to spend. Shift-Return is deliberately not advertised — it is
the escape hatch from the hint, not a second thing to learn.
Click-through for box 2
documents in it — chips that name them. Tapping one fills the field and focuses it.
↩appears. Delete it: it goes.Box 3 — tool cards readable at island width
Two defects in
ToolExecutionCard.formatArguments, both invisible in a full window andboth plain on a 700pt card whose row has to hold the summary, the status, and the
Approve and Deny buttons at once.
It was unordered. It mapped over a
Dictionary, which has no order, so the samecall rendered
query: standup, limit: 5on one pass andlimit: 5, query: standuponthe next. With the line truncated, the argument you can actually read changed as the
view re-rendered. Keys are sorted now — alphabetical is arbitrary, but it is the same
arbitrary every time, which is the property that matters when you are comparing one
card with the one above it.
It was unbounded.
update_documentcarries the whole new body as an argument, sothe string handed to
Textwas the length of a document — 20,009 characters in thetest that now guards it.
lineLimit(1)hid the tail, but the text was still laid out,pushing everything after it on the row for room that was never going to be used. Values
are flattened onto one line too, so a multi-paragraph argument stops looking like a
short one with no ellipsis to say otherwise.
The approval buttons no longer compress. How to refuse must never be what gives way,
and the summary beside them is already truncating.
Tests
9 cases in
ToolArgumentSummaryTests. Mutation-checked both ways:dict.keys.mapinstead of.sorted()Worth saying plainly: "The same arguments always produce the same line" does not on
its own catch the ordering regression — a dictionary's iteration order is seeded per
process, so within one run the old code answered consistently too.
keysAreSortedisthe one that catches it. There is a note in the file saying so, because a test that
looks like a guard and is not is worse than no test.
Click-through for box 3
card: the argument line truncates in the middle and the row's controls stay put.
unsqueezed however long the tool name is.
Box 4 — approval prompts that say what they will do, and to what
Every destructive tool takes a UUID. The card had five hand-written sentences and a
fallback of
"Agent wants to run \(toolName)", and none of them named the target — so"Agent wants to delete a document" was the whole of what the user was told before
being asked for Touch ID. Which document was not knowable from the card at all.
Now:
Delete “Q3 Planning”.Write to “~/notes/todo.md”.Draft an email to “sam@example.com”. Anddelete_space, which trashes every document and meeting insideit and its children, says so:
Delete, with everything in it, “Work”.ToolApprovalPromptholds the wording and which argument carries the target, pure soboth are testable;
ToolApprovalTargetResolveris the half that asks the stores.Reminders and calendar events deliberately resolve to nothing. Both live behind an
EventKit permission, and prompting for access as a side effect of drawing an approval
card is not acceptable in the one card whose job is to be trustworthy. They show the
action alone — honest about not knowing the name — rather than a UUID, which tells the
user nothing and reads as a bug at the moment they are deciding whether to trust the
agent.
The coverage test found two on its first run
It walks the real registry, so a tool that asks for approval without a sentence is a red
build rather than a card reading "Run delete_everything" over a Touch ID button. On its
first run it failed with:
Both read private data. Both had been rendering as "Agent wants to run get_location".
Neither was on my list — the test was.
AgentCoordinatorgainedallKnownTools()so that walk cannot go stale, and theregistry is now built from it rather than from a second list. No behaviour change: web
tools are filtered out unless opted in, exactly as before, and they are not in the
Settings per-tool list so the ordering of the disable filter is unobservable.
The approval sentence also renders as primary text over two lines — a title cut in half
is how you approve the wrong thing. Every other card stays on one line, because it is a
record of something that already happened.
Click-through for box 4
Reject leaves it alone.
is about to be read.
Box 5 — the gap between a send and the first token
The island had nothing there. Between pressing Return and the first token it showed
the user's own bubble and empty space — which is exactly when someone concludes the send
did not land and presses Return again. Once the assistant message existed it showed a
literal
"..."pushed through the markdown renderer, in a grey bubble.It now shows the pulsing row the main window shows, saying what the agent is actually
doing — "Searching the web…", "Reading…" — rather than a static "Thinking…".
The wording already had one definition in
UICopy.Status.describe(toolName:). When toshow it did not: it was written out longhand at each place that needed it, and came out
differently at each.
AgentThinkingStateis that half, and both surfaces read it — themain window's list indicator included.
The input worth naming
pendingAnswerTextis what has arrived of the answer being produced now, not "thelast assistant message". The latter is the previous answer and is non-empty for the
whole of every later gap, so an indicator keyed on it appears once and never again.
There is a case pinning exactly that, because it is the mistake the shape invites.
A tool card also suppresses the row: two things claiming to explain the same pause is
worse than one, and the card is the more specific, since it names the tool.
Click-through for box 5
pulsing row, not a gap and not three dots in a box.
card, then to the answer.
assistant message it would not.
Box 6 — chips that do not steal the bar's line
The island floated its chips in an
.overlayoffset 34pt above the pill. Twoconsequences:
bottom of the transcript.
and off both ends.
The row is now in the
VStack, andComposerChipRowdecides what fits.Modes are never hidden
The ordering rule is the part worth stating. A mode chip says what the send is about to
do — search the web, spend minutes on Deep Research — and a hidden one is a send the
user did not know they were making. So a limit too small to hold the modes is exceeded
rather than met by dropping one, and there is a case pinning that.
Attachments overflow into a
+3 morecounter instead, which loses nothing: the filesare still attached. The counter also pays for its own slot — four slots and six files
gives three chips and "+3 more", not four chips and "+2 more", which would be five
things in a four-slot row.
The counter carries the names it hides, as its tooltip and as its VoiceOver label. A
chip that hides a file with no way to find out which one is worse than a row that
overflows.
Modes are a value now rather than two booleans added up wherever a count was needed.
Tests
9 cases, including a sweep asserting
shown + hidden == totalacross every combinationof 0–40 attachments and 0–2 modes — nothing is ever silently dropped.
Click-through for box 6
the counter drops.
and the pill rather than on top of the transcript.
still there.
Box 7 — VoiceOver labels, and the keyboard path
Almost every control in the island carried only
.help(…). On macOS that becomes theaccessibility hint, not the label, so VoiceOver fell back to the only other thing it
had — the SF Symbol name. The send button announced "arrow up". The close button
announced "xmark". And the two mode toggles announced nothing at all about being on,
which is the single thing a toggle exists to convey.
IslandControlCopyholds the name, the state and the tooltip together so they cannotdrift, and one
.islandControl(…)modifier applies all three. Writing them outseparately at each call site is exactly how one gets forgotten — which is how this
happened in the first place.
Send and Stop are named differently rather than being one control in two states. A
button that keeps the name "Send" while it cancels is how someone stops a run they meant
to let finish.
The test
It walks every control in every state and rejects a dotted, lowercase label — the shape
an SF Symbol name has. Mutation-checked: restoring
"arrow.up"on the send button turnsit red on both assertions.
Keyboard: Escape already dismisses the island (handled in
CommandCenterController),Return sends, Shift-Return inserts a newline and ⌘Return sends from anywhere in the
field. Those were in place; what was missing was everything above.
Click-through for box 7
the two toggles announce On or Off.
Box 8 — Reduce Motion
The island is the most animated surface in Logue: it slides in over another app, springs
open on the first message, scales its chips in and out, and runs two forever pulses.
None of it consulted Accessibility → Display → Reduce motion, so someone who asked
the system for less movement got the most of it here.
IslandMotionis the policy, and it turns on decoration versus information.A spring, a slide and a scale say nothing the layout does not already say, so they go —
replaced by a cross-fade rather than by nothing, because removing the transition outright
turns every panel into a jump cut, which is not what the setting asks for.
A pulsing dot is the harder case: it is the only thing on screen saying the agent is
still working. It stops anyway — but only because every row it appears in also says so
in words. Removing it from somewhere without that text would be removing the signal, not
the decoration.
The level meter is deliberately untouched. Its movement is the reading. A still
meter is not a calmer meter, it is a broken one.
Springs are removed rather than shortened: a spring is the movement, and making it quick
makes it a flinch.
PulsingDotreads the setting itself rather than at each call site, so a dot addedsomewhere new cannot reintroduce a forever-repeating animation. That also fixes it in the
main window, which had the same gap.
Click-through for box 8
springs, chips fade rather than scale, and the thinking dot is visible but still.