The awkward hand-off was not the dwell gate — it was committing at whatever shift the thumb left. quickstep travels to the end target first; settleTo does that now. Dwell restored to match prior art. Records the geometry being measured rather than assumed, and two findings that are not this task's: furniture_at() is a stub so synthetic touch cannot reach the pill, and Origin is two-valued so agent touches carry no audit.
444 lines
24 KiB
Markdown
444 lines
24 KiB
Markdown
# TASK 60 — multitasking is a place, not a transition
|
||
|
||
**Status: 2026-08-07 (latest) — both halves are built and the strand repro
|
||
passes on hardware.** The compositor owns the transition geometry; the shell
|
||
drives it through four verbs and names the card rect once. Read "One owner" for
|
||
why, then "Shell half: DONE" for what landed.
|
||
|
||
## One owner — the rebuild, 2026-08-07
|
||
|
||
### The fault, stated once
|
||
|
||
**Two writers own one geometry.** The rail scales the real windows through
|
||
`pose`; `ZoneOverview` scales its cards through `scale`. Both read
|
||
`zonePullProgress`, and a session was spent trying to make the two numbers
|
||
agree — the card ran `0.6 → 1.0` while the pose ran `1.0 → 0.6`, so at the
|
||
handoff the window was at 0.6 of the glass and the card arrived near full
|
||
bleed.
|
||
|
||
That is `DEVICE-STATE-MACHINE.md` §1's blind actors in the render path, and
|
||
TASK-52 names the same rule for exactly this reason: *one attention model, one
|
||
clock, effects as strategies over it*. Every symptom is the one fault — the
|
||
size pop, the doubled frame (viewtop's 3 px `#9EC7FF` under the card's
|
||
`radius: 18`), and the strand.
|
||
|
||
**The strand is the proof.** Casey, 2026-08-07: open btop, swipe to
|
||
multitasking, tap Home, reopen multitasking, tap btop — btop comes back as a
|
||
small card in the middle of the screen. Every clear-the-pose path built that
|
||
day hangs off the rail's drag edge (`endPull` → `clearPose`). *Those taps are
|
||
`ZoneOverview`'s and never touch the rail*, so nothing ever runs. A transform
|
||
that one surface applies and a different surface must remember to undo will
|
||
always have a path that forgets. Note also that `pose` is TASK-52 **#10,
|
||
gravity wells** — an atmosphere primitive that navigation borrowed.
|
||
|
||
### What the prior art does — quickstep, on this machine
|
||
|
||
`~/Downloads/lineage-trebuchet/quickstep/src/com/android/quickstep/`.
|
||
|
||
- **One float.** `SwipeUpAnimationLogic.mCurrentShift` ∈ [0,1] — its own comment:
|
||
0 = preview snapshot completely visible, 1 = *"preview snapshot completely
|
||
aligned with the recents view"*. `TaskViewSimulator` transforms the **live
|
||
window onto the real card rect**. The card never has to be made to match the
|
||
window, because the window is placed on the card. There is no second geometry.
|
||
- **The drag length is derived from the destination.**
|
||
`getSwipeUpDestinationAndLength(dp, ctx, TEMP_RECT, …)` returns the rect and
|
||
the length together, and `mDragLengthFactor = dp.heightPx / mTransitionDragLength`.
|
||
Our `missionAt` is 18% of the panel, a number with no relationship to where
|
||
the card actually is.
|
||
- **End targets are an enum**, not a set of booleans: `HOME, RECENTS, NEW_TASK,
|
||
LAST_TASK, ALL_APPS` (`GestureState.java:77`). Every gesture resolves to
|
||
exactly one, and **`LAST_TASK` is a first-class destination** — "went back to
|
||
the app" is a real endpoint that animates, not an abandon branch.
|
||
- **The controller owns the transform and hands it back at the endpoint.**
|
||
Nothing hopefully-undoes anything, which is structurally why Android cannot
|
||
strand a window and we can.
|
||
- **Dwell is first-class.** `isFling = mGestureStarted && !mIsMotionPaused &&
|
||
|endVelocity| > threshold`, and in gestural mode `mIsMotionPaused → RECENTS`
|
||
(`AbsSwipeUpHandler:1121,1322`). A pause is never a fling.
|
||
`MIN_PROGRESS_FOR_OVERVIEW = 0.7`.
|
||
|
||
Sailfish/Glacier (`~/phone-qml-research/glacier-home`) is the QML-side reference
|
||
for the same shape and is worth reading before the shell half.
|
||
|
||
### The plan
|
||
|
||
1. **One owner of the geometry.** The compositor owns the transition: one verb
|
||
carrying `shift` 0→1, where 1 means the window sits exactly on its card rect.
|
||
The destination rect is named once at gesture start, not re-derived per
|
||
frame. The shell stops calling `poseActiveZone` for navigation and stops
|
||
scaling cards; `ZoneOverview` draws chrome — label, close control,
|
||
neighbours — and never a competing transform.
|
||
2. **End targets as an enum.** One end-target state replaces
|
||
`missionControlOpen` / `overviewOpen` / `zonePullProgress`. Entering and
|
||
leaving become the same machinery whether the gesture came from the pill or
|
||
from a card tap — which is precisely what the strand repro broke. `LastTask`
|
||
is a destination, so the transform is released by *arriving*, not by
|
||
remembering to clear.
|
||
3. **Detents from the destination rect**, not 18%. Keep the dwell; make
|
||
"paused is never a fling" explicit rather than emergent.
|
||
4. **One frame.** A window inside the transition is furniture in the overview's
|
||
composition, so `focus_border` suppresses itself while a window is owned by
|
||
the transition and the card carries the chrome.
|
||
|
||
### Compositor half: DONE 2026-08-07 (`souveraine-viewtop` `3c85397`)
|
||
|
||
Gates green on archdev — 239 tests, `cargo fmt`, workspace clippy and
|
||
`--features kms` clippy, all `-D warnings`. Not yet on the phone: needs a CI
|
||
package and a session restart.
|
||
|
||
- `crates/compositor/src/transition.rs` — the state. `begin(&[TransitionTarget])`
|
||
/ `shift_to(0..=1)` / `commit(EndTarget)` / `cancel()` / `forget(id)`, seven
|
||
unit tests. Empty `begin` refused; `forget` on the last leg ends the flight;
|
||
`forget` is wired to surface destruction beside `navigation.forget`.
|
||
- Wire: `OverviewBegin { to } / OverviewProgress { progress } / OverviewCommit
|
||
{ target } / OverviewCancel`, plus `TransitionTarget` and `EndTarget`
|
||
(`home | overview | last_zone | zone{zone}`), all four in the verb table.
|
||
- Geometry: `Viewtop::carried(window)` returns the lerped origin and scale, and
|
||
**`window_origin` and `window_extent` both consult it**, so the render and the
|
||
hit test inherit the carry with no call-site changes. Anchor is `(0,0)` on
|
||
purpose so `pose_origin`'s pivot term vanishes.
|
||
- `focus_border` returns empty while a window is carried — the card owns the
|
||
frame.
|
||
|
||
### Shell half: DONE 2026-08-07 (`souveraine`)
|
||
|
||
`services/ZoneTransition.qml` is the one owner: the card rect, the four verbs,
|
||
the end-target table, and the clock. `ZoneOverview` and the rail both read it
|
||
and neither derives a geometry.
|
||
|
||
**Deleted, not adapted** — `ViewtopControl.poseActiveZone` / `clearPose` /
|
||
`_posed`, `zoneCard.scale`, and `GlobalStates.zonePullProgress`. The card's
|
||
inner `ColumnLayout` went too: its 8 px margin was a second opinion about where
|
||
a window sits inside its zone. Panes are now the compositor's own tiling scaled,
|
||
so a split zone draws as what it is.
|
||
|
||
**One clock, two strategies.** The rail reports travel and interprets nothing.
|
||
`ZoneTransition` derives `shift` (what the compositor carries on, clamped) and
|
||
`presence` (how present the destination is, rising to the detent then receding
|
||
past it, so a home-bound pull never previews somewhere the release will not go).
|
||
Two curves is correct; two *places* was the bug, and putting the second one on
|
||
the rail was the first thing tried here and reverted. TASK-52's rule — one
|
||
clock, effects as strategies over it — is the same rule as "one owner", a level
|
||
up, and `pose` is a gravity well borrowed from those primitives, so this is her
|
||
felt environment, not merely navigation chrome.
|
||
|
||
#### What actually made the swipe feel wrong
|
||
|
||
Not the dwell gate. **The release committed where the thumb left it.** Lift at
|
||
60% of the climb and the carry was released at 60%, so the window jumped to its
|
||
final state with no travel — two motions with a cut between them. Every attempt
|
||
to fix this by tuning fades was treating the symptom.
|
||
|
||
quickstep does not do that (`AbsSwipeUpHandler.handleNormalGestureEnd`):
|
||
|
||
```java
|
||
float endShift = endTarget.isLauncher ? 1 : 0;
|
||
long expectedDuration = Math.abs(Math.round((endShift - currentShift)
|
||
* MAX_SWIPE_DURATION * SWIPE_DURATION_MULTIPLIER));
|
||
duration = Math.min(MAX_SWIPE_DURATION, expectedDuration);
|
||
startShift = currentShift;
|
||
```
|
||
|
||
`ZoneTransition.settleTo()` is that, with the same numbers — 350 ms cap,
|
||
multiplier `min(1/0.7, 1/0.3)` from `MIN_PROGRESS_FOR_OVERVIEW`. Every release
|
||
travels to its destination and commits on arrival, including the abandoned
|
||
half-swipe (`last_zone`, end shift 0), which is the commonest gesture of all and
|
||
used to snap.
|
||
|
||
Correcting a claim made earlier in this session: the dwell gate was **restored**
|
||
after reading the source. quickstep gates the same way and the "ever" is a latch,
|
||
not a level:
|
||
|
||
```java
|
||
recentsAttachedToAppWindow = mHasMotionEverBeenPaused || mIsLikelyToStartNewTask;
|
||
```
|
||
|
||
Removing it went *against* prior art. A fast unpaused flick showing no
|
||
destination on its way past is correct.
|
||
|
||
#### Measured on hardware (`10.10.30.213`, viewtop `r97.g3c853972`)
|
||
|
||
- The carry lands where it is told: target `(135,280) 270×520` logical →
|
||
`(271,561) 539×1035` physical at scale 2.0. `focus_border` correctly absent.
|
||
- Shell-driven: card `(73.9, 12.0) 392.2×784.4`, scale 0.7263; carried window
|
||
measured `(148,89)–(931,1590)` against predicted `(147.8,82.1)–(932.2,1592.7)`.
|
||
- **The strand repro passes**, three times across three shell builds.
|
||
|
||
#### The geometry is measured now, not assumed
|
||
|
||
`ZoneTransition` took the card's box from `screen.height * 0.78`. The overview
|
||
`PanelWindow` respects exclusive zones, so its height changes underneath: read
|
||
once as **1040** (something reserving 40 px) and once as **1080**. The assumption
|
||
was right today and would have drifted silently the moment the dock reserved.
|
||
The surface reports its own rect through `measuredAt()`, and `carryState` prints
|
||
`surface` beside `panelWindow` so a card in the wrong place is *read*, not
|
||
theorised — the same reason `state` exists at all.
|
||
|
||
#### Two findings that are not this task's
|
||
|
||
1. **A synthesized touch cannot reach the pill.** `furniture_at()` returns `None`
|
||
unconditionally and deliberately (`wayland.rs:4490`: *"until the engine
|
||
publishes a region layout, nothing is furniture and every touch is session
|
||
content"*), so an agent's `touch_down` on the rail lands on the client window
|
||
behind it. This is the structural argument for "it has to be a verb" below —
|
||
the touch path *cannot* drive the pill today. A drawn hand (Casey, 2026-08-07,
|
||
art in progress) rides on the verbs and does not wait on this.
|
||
2. **`Origin` is two-valued**, so on-device Ani and a remote caller over the
|
||
socket are indistinguishable, and an agent touch leaves no trail at all —
|
||
`is_evidence()` is false by design. Evidence ("does the machine think a human
|
||
is here") and audit ("who did what") are different questions and only the
|
||
first is answered. TASK-41's attested producers is where the second lives.
|
||
|
||
Still unseen on glass: a split zone's card, which is right by construction
|
||
(each pane is the compositor's reported rect scaled) but has not been exercised.
|
||
|
||
### Shell half: the plan as written (kept for the argument)
|
||
|
||
Rewrite the pill and `ZoneOverview` to drive the four verbs instead of posing
|
||
windows and scaling cards independently.
|
||
|
||
1. `ZoneOverview` computes each zone card's rect in panel coordinates. That rect
|
||
— not `0.6`, not an inset — is the `TransitionTarget` for the windows on that
|
||
zone.
|
||
2. `SystemGestureRail.onPressed` → `overview_begin` with those rects.
|
||
`onPositionChanged` → `overview_progress`. Release → `overview_commit` with
|
||
the end target chosen from projected travel and dwell, or `overview_cancel`.
|
||
3. **Delete** `ViewtopControl.poseActiveZone` / `clearPose` / `_posed` and
|
||
`GlobalStates.zonePullProgress`'s pose duties, and drop `zoneCard.scale`
|
||
entirely — the compositor is now carrying the real window onto that rect, so
|
||
a card that also scales is the second writer all over again. The card becomes
|
||
chrome: label, close control, and the neighbours either side.
|
||
4. Tapping a card is `overview_commit { target: zone }` — which is what makes the
|
||
strand repro pass, because that path now releases like every other.
|
||
|
||
Today's four shell commits (`57208c8`, `f699f57`, `452acea`, `72171ac`) are the
|
||
two-writer design and step 3 deletes most of them. Kept on the phone meanwhile
|
||
because the sizes at least agree.
|
||
|
||
### It has to be a verb, because she gives the tour
|
||
|
||
Casey, 2026-08-07: there is a point where an agent is asked for a tour or a demo
|
||
of the phone, *"and that does mean even these little gesture steps will be
|
||
possible."* So the transition is a **verb with a caller**, not a side effect of a
|
||
touch handler — `shift` and the end target are both reachable from the verb
|
||
table, or a tour is impossible and TASK-30's third capability enumeration grows
|
||
a fourth.
|
||
|
||
The principles that already govern her hand govern this too, and they are built
|
||
(`input.rs`):
|
||
|
||
- `Origin::{Physical, Agent}`, carried from the source, never a caller-set field.
|
||
- `AGENT_SLOT_BASE = 1 << 16`, so a synthesized contact cannot land on a slot a
|
||
thumb owns.
|
||
- `Origin::is_evidence()` — **an agent-driven tour must not be evidence.** A demo
|
||
that raises `observed_confidence` or feeds the idle budget is the machine
|
||
believing a human is present because she moved a window.
|
||
- Step-up stays human-only, and already is: `input.rs:432` routes
|
||
`Origin::Physical => Route::LockSurface` and `Origin::Agent =>
|
||
Route::Withheld("the lock surface takes fingers, not the agent")`. The
|
||
navigation verb must not become a way around that.
|
||
|
||
Connects to TASK-50 (the same origin machinery, and the inverse mapping `pose`
|
||
owes the hit test) and doctrine §13 (the 60 is hers; the credential is the 40).
|
||
|
||
## 2026-08-07 — the cards were zero pixels wide
|
||
|
||
Reported as *"it goes to a blur screen but nothing else"*, and every signal read
|
||
healthy: `zones 2, windows 1, subscribed true, loader active, item present,
|
||
opacity 1, progress 1`. The blur was the backdrop; the cards were there and
|
||
**540 px shorter than nothing** — `item.w: 0, h: 842`.
|
||
|
||
`ZoneOverview` took its width from the `Column` it sits in, and a Column is as
|
||
wide as its widest child. In the drawer the search widget supplies that width;
|
||
**mission control is the cards alone and has no search**, so the only child left
|
||
was the loader — whose width came from the column, whose width came from the
|
||
loader. The cycle resolves to zero. Bound to `panelWindow.width` instead, which
|
||
is what this surface covers anyway (its mask is `missionBackdrop`, not the
|
||
drawer's sheet).
|
||
|
||
Two things that made it findable, and are worth keeping:
|
||
|
||
- **`overview missionControl` and `overview state` verbs.** The one surface
|
||
reported broken was the only one nothing but a pill swipe could raise, so it
|
||
could not be inspected or tested. `state` reports what the cards are drawn
|
||
from, and reading `item.w: 0` beside `column.w: 0` is what located this in
|
||
one step after a long time spent on wrong theories.
|
||
- **A false lead worth recording:** the compositor was refusing every capture
|
||
with `"no client captures the screen behind a lock screen"`, and `grim` hung
|
||
the same way the cards did. That is *correct* behaviour behind a lock — the
|
||
session had been left locked by a test. Capture was never the problem. Check
|
||
`locked` in `state` before concluding anything from a capture failure.
|
||
|
||
**Cards are inset** (26 px sides, 12 top) so neighbouring zones show through —
|
||
full-bleed reads as "you are looking at that app" rather than "here are the
|
||
places you can go", and leaves nothing on screen saying there ARE neighbours.
|
||
|
||
### Still open: the swipe in
|
||
|
||
Casey, 2026-08-07: *"the visual style from swiping up into this mode... we lost
|
||
the consistency... the swipe to this swap is awkward."*
|
||
|
||
The machinery exists and is where to look. `ZoneOverview.progress` follows
|
||
`GlobalStates.zonePullProgress` while the rail is pulling and the state flag
|
||
takes over on commit — this task's own acceptance is *"no frame where a window
|
||
is scaled by one and laid out by the other."* Two concrete suspects:
|
||
|
||
1. Cards start at `scale 0.6 + 0.4 * share`, chosen to match the scale the
|
||
rail's `pose` has reached at the multitasking detent. **0.6 is a
|
||
written-down constant**; if the rail's actual pose there is not 0.6, the
|
||
card pops to a different size at handoff.
|
||
2. The inset above makes the card **smaller than the full-bleed window it
|
||
replaces** at the handoff moment, which may have made that seam worse.
|
||
Either the rail's pose should end at the inset card's rect, or the inset
|
||
should animate in after the handoff rather than being there at `share = 0`.
|
||
|
||
Needs watching in motion. Screenshots cannot see it.
|
||
|
||
## Original — design, 2026-08-05
|
||
|
||
Written after building the wrong thing and being told so. Casey, on device:
|
||
|
||
> *"Still major issues. We might need to take these concepts we've built back to
|
||
> the workshop. The scaling doesn't follow the selected app. meaning app#2
|
||
> opened even if focused is not scalling down. Second, all we have is scaling;
|
||
> it's supposed to be showing me all sorta backgrounded zones; including split
|
||
> zones. You've seen macOS. That whole multitasking view is a function of it's
|
||
> own."*
|
||
|
||
**Repo:** `souveraine` (shell surfaces), `souveraine-viewtop` (capture + zone
|
||
facts).
|
||
|
||
## What was built, and why it is not the thing
|
||
|
||
The rail's short swipe scales the live windows on the active zone from 1.0 to
|
||
0.6 as the thumb climbs (`ViewtopControl.poseActiveZone`), then opens
|
||
`GlobalStates.missionControlOpen`. That is a **transition** — the animation of
|
||
leaving an app — and it was mistaken for the **destination**.
|
||
|
||
Two failures follow from that mistake, and both were seen on hardware:
|
||
|
||
1. **It scales the wrong windows.** `poseActiveZone` poses everything whose
|
||
`workspace` matches `ViewtopControl.activeZone`, and that list comes from a
|
||
**2-second poll** of `{"op":"workspaces"}`. Open a second app and the poll
|
||
has not caught up, so the newly focused window is not in the set being
|
||
scaled. There is no "focused window" fact in the shell at all — the
|
||
compositor knows it (`Surface::focused`) and does not report it in the
|
||
`workspaces` reply.
|
||
2. **Scaling one zone is not a view of all of them.** Even done perfectly, it
|
||
shows the zone you are already on, smaller. It cannot show what is
|
||
*backgrounded*, which is the entire question multitasking answers.
|
||
|
||
## What exists
|
||
|
||
| Piece | Where | State |
|
||
|---|---|---|
|
||
| `WindowOverview.qml` | `modules/souveraine/navigation/` | One column of equal cards, one per **window**, live `ScreencopyView` texture, flick-up to dismiss |
|
||
| Zone facts | `{"op":"workspaces"}` | `count`, `active`, `offset` (the float the strip is scrolled to), `settled`, and per window `{id, workspace, at, size}` |
|
||
| Per-window capture | viewtop, since 2026-08-03 | A card can show a window rather than the screen covering it |
|
||
| `pose` / `unpose` | viewtop | Now genuinely scales content (fixed 2026-08-05 — it had only ever moved it) |
|
||
|
||
**`WindowOverview.qml`'s own header states the gap**: *"viewtop has them
|
||
[workspaces], and they are not Hyprland's: a continuous strip that grows when a
|
||
window needs a room and shrinks when the last one leaves… That is a thing this
|
||
overview could draw and does not yet."* This task is that sentence.
|
||
|
||
## What macOS actually does, since it is the named reference
|
||
|
||
Mission Control is **two levels in one surface**: a row of *spaces* across the
|
||
top, and the windows of the current space spread below. Neither alone is the
|
||
feature — the row answers "where else am I?" and the spread answers "what is
|
||
here?". Dragging a window onto a space in the row moves it there, which is the
|
||
same gesture doing placement.
|
||
|
||
Ours differs in one load-bearing way and it must not be papered over: **macOS
|
||
spaces are a fixed set the user creates; viewtop's zones are minted and
|
||
destroyed by need** (`workspace.rs`: "a workspace exists because something is on
|
||
it"). A row of zones is therefore a row that grows and shrinks under you, and
|
||
home is the one that is always there and always empty.
|
||
|
||
## The questions
|
||
|
||
### Q1. What is a card — a window, or a zone?
|
||
|
||
Today it is a window. macOS says both, at two levels. A phone has room for one
|
||
level at a time. Options: zones-as-cards with their windows composited inside
|
||
(split zones then read correctly, which is Casey's explicit ask); windows-as-
|
||
cards grouped by zone; or a two-level surface where the zone row is a strip and
|
||
tapping one spreads its windows. **Q's real content:** a split zone has two
|
||
windows and must look like one thing you can switch to, not two.
|
||
|
||
### Q2. Live textures, or posed real windows?
|
||
|
||
Two mechanisms now exist and they are not the same:
|
||
|
||
- `ScreencopyView` — a live *picture*. Cheap to lay out anywhere, but it is not
|
||
the window; input goes to the card.
|
||
- `pose` — the *real* window, scaled, still touchable through `unpose`'s inverse
|
||
mapping.
|
||
|
||
The reference compositor composes client windows as external textures directly
|
||
into the shell's scene, which is the third answer and the one the in-process
|
||
Flutter shell would make natural (`SHELL-BOUNDARY.md`). Choosing here decides
|
||
whether the overview is a gallery of pictures or the actual desktop, zoomed
|
||
out — and whether the transition can be continuous into it.
|
||
|
||
### Q3. Where does the focused window come from?
|
||
|
||
The shell has no such fact. `Surface::focused` exists in the compositor's
|
||
registry and is not in the `workspaces` reply. Any answer to Q1 needs it, and it
|
||
should be reported rather than inferred — inferring "the last window I saw
|
||
open" is the kind of shadow copy §4 spends its whole argument against.
|
||
|
||
### Q4. Is the poll acceptable?
|
||
|
||
`ViewtopControl` polls every 2 s. That is already the proximate cause of failure
|
||
1. A view built on it will be wrong every time something changes faster than
|
||
two seconds — which is every gesture. Either the compositor pushes zone/window
|
||
changes, or this surface asks synchronously when it opens and stops guessing in
|
||
between. **Design the push channel; it is owed anyway.**
|
||
|
||
### Q5. What does the transition become?
|
||
|
||
If the destination is a real view, the scale-on-drag is either (a) the first
|
||
frames of it, continuous into the cards, or (b) noise that should be deleted.
|
||
macOS and Phosh both do (a) — Phosh's `home.c` is a drag surface travelling
|
||
between two states, not a button. (a) is better and harder: it means the
|
||
transition and the view share one progress value, which is the reference
|
||
overview's whole trick (`WindowOverview.qml` already cites it: "one `progress`
|
||
driving everything").
|
||
|
||
### Q6. What can you do to a card?
|
||
|
||
Switch to it, certainly. Close it — the `close`/`kill` verbs exist. Move a
|
||
window between zones — `{"op":"workspace","to":N,"surface":id}` exists too, so
|
||
drag-a-card-onto-a-zone is reachable today. Decide before building, because it
|
||
changes whether cards need to be drag targets.
|
||
|
||
## Acceptance (measured, on the phone)
|
||
|
||
- Every zone with something on it is represented, and a **split zone reads as
|
||
one destination containing two windows** — not as two unrelated cards.
|
||
- The view reflects state at the moment it opened, not up to two seconds stale.
|
||
Open an app, immediately open multitasking: the new app is there.
|
||
- Tapping a card goes to that zone. Home is reachable from it.
|
||
- Whatever drives the transition and whatever draws the view agree — no frame
|
||
where a window is scaled by one and laid out by the other. Met by there being
|
||
one geometry, not by two that were tuned to match.
|
||
- The scale-on-drag either continues into the view or is gone. Not both.
|
||
- **The strand repro passes.** btop → multitasking → Home → multitasking → tap
|
||
btop, and btop is full size. No path leaves a window transformed, including
|
||
the ones that never touch the rail.
|
||
- **The gesture is reachable as a verb.** An agent can drive `shift` and pick an
|
||
end target well enough to demo multitasking, and the run leaves
|
||
`Origin::Agent` throughout: no evidence, no idle-budget credit, no route to
|
||
the lock surface.
|
||
|
||
## Connects to
|
||
|
||
TASK-14 (the overview pane and its app grid; "a grid of workspace thumbnails is
|
||
dead weight on a phone" is the constraint that made cards windows in the first
|
||
place), TASK-55 (the sheet, and `ViewtopControl` as the one path to scene
|
||
verbs), TASK-43 (the compositor's surface model and per-window capture), TASK-52
|
||
(atmosphere — `pose` is a gravity well and this is its first real user),
|
||
`VIEWTOP-AND-DENIAL.md` (external textures into the shell's scene; Q2's third
|
||
answer), `SHELL-BOUNDARY.md` (the in-process shell that would make it natural).
|