| Age | Commit message (Collapse) | Author |
|
compounding per nesting level
|
|
last-row nested box so spacing stays even
|
|
parent reverts to half width when its subagents close
|
|
(add/remove from group)
|
|
(mobile) sidebar
|
|
gap-matched bottom spacing; dvh app-shell height for iOS toolbars
|
|
secondary actions as text links, Phosphor icons when compact
|
|
shared navbar look on the home header
|
|
views) and darkstrap navbar header in theme colours
|
|
deep imports)
|
|
thinking-style dots while running
|
|
rel=noopener noreferrer)
|
|
failed/orphan/pending headers
|
|
|
|
|
|
|
|
on-track pace markers
Consumes GET /usage (transport-contract 0.25.0). DaisyUI progress bars per quota
window with a dot at the elapsed fraction of the window (the 'on track' level),
ahead/under-pace labels, reset countdowns, stale/unavailable/not-configured states.
|
|
- Batch live chat events: fold a burst in one pass per animation frame
(rAF + timeout fallback for hidden tabs) instead of one reactive cycle per
WS frame. Flushed before send/resync to keep ordering.
- De-dup the late-join replay against committed history: provisional chunks
of the in-flight turn already persisted (CR-6 per-step persistence) are
hidden by aligned-prefix matching (fail-safe: mismatch shows, never hides).
- Memoize chunks/turnMetrics getters by state identity.
- LazyCollapse: tool/thinking card bodies mount only on first open.
- content-visibility:auto on transcript rows.
- scripts/probe-replay.ts + bench-pure.ts for measuring the replay path.
|
|
|
|
step-level retention
Placement
- Chunks carry their producing turn: StoredChunk.turnId (backend [email protected]) for
committed rows (persisted in the IDB cache; old cached rows are upgraded on
sync) and the live event's turnId for provisional ones (the optimistic echo is
tagged on turn-start).
- interleaveTurnMetrics attributes every group to its turn (turnId, else the
turn owning its stepId) and puts the turn total after the turn's LAST visible
group. No user-message counting: sending, streaming, steering, images, errored
turns and chat-limit unloading can no longer shift metrics between turns — at
any point during a turn, not just after it ends. A turn cut in half by the
chat limit keeps its total under its visible tail.
- Legacy rows (no turnId) fall back to an order-preserving, count-guarded match
that leaves a turn blank rather than show another turn's numbers.
- A turn that errors re-syncs metrics (the backend now persists them).
TPS
- Step TPS = outputTokens / decodeMs only (no genTotalMs fallback — it includes
time-to-first-token). Turn TPS sums tokens and decode time over timed steps
only. Tool execution is excluded by the backend.
Display
- Retention = this turn's first-step cache reads / previous turn's last-step
prompt (step-level; turn sums made it pin at 100%).
- Turn row shows output tokens + input processed (with cached), instead of a
misleading 'total tok' that summed every step's full prompt.
|
|
- New features/search-settings: pure view-model (options = available backends
∪ 'None (search disabled)', unconfigured entries disabled with a suffix,
unknown persisted selection prepended, network-seam normalization) + the
SearchBackendField dropdown (save-on-change, spinner/saved/error states,
revert-to-persisted on failure, 503 -> disabled field with a 'search not
available' hint, loading skeleton, help text noting search-mode-only scope).
- store.svelte.ts: searchBackendConfig() / setSearchBackend() over the global
GET/PUT /search/config routes (503 marked unavailable on the failure variant).
- SettingsPane renders the field under the chat limit; ExtensionsPane lists
the new module manifest.
- .dispatch mirrors: wire + transport-contract deltas for the new types/routes.
|
|
Consume the backend's archive contract (feature/archive-workspaces):
- adapter/http: archive(id)/unarchive(id) (PUT/DELETE /workspaces/:id/archive);
list(archived?) passes the ?archived= query
- logic/view-model: pure applyArchived(list, id, archived)
- store.svelte: setArchived(id, archived) (optimistic + revert-on-error +
reconciling refresh); archivedList + refreshArchived() for the archived view
- ui/TileActionBar: new Archive action (the right-click→select action bar)
- ui/HomeHeader: new Archived button opens the archived view
- ui/ArchivedWorkspaces (new): modal listing archived workspaces with
Unarchive + View (navigates to /<id>)
- ui/WorkspacesHome: wire the Archive action + the Archived modal
- .dispatch/wire.reference.md + backend-handoff.md: record the contract delta
|
|
per-image toggle
Staged images now default to a compressed variant: LOSSLESS sources
(PNG/BMP) are re-encoded to JPEG at q0.85, rescaled to cap the longest
edge at 2048px (never upscaled, high-quality smoothing, alpha flattened
onto white), and used only under a smaller-wins rule. JPEG/WebP/GIF are
sent verbatim. Each staged thumbnail gets a Compress checkmark (default
ON, reversible until send, cached so flipping never re-encodes); OFF
sends the original untouched bytes. Conversion runs async at staging and
any failure silently falls back to the original — sending is never
blocked or broken.
Pure decision logic lives in features/chat/logic/image-compress.ts
(format selection, dimension math, smaller-wins); the canvas encode is a
thin injected edge (features/chat/adapter/image-encode.ts, Composer prop
encodeJpeg). No wire-contract, send-path, or backend changes; complements
the backend's body-budget eviction safety net by shrinking payloads at
the source.
Verification: typecheck 0 errors, vitest 1720/1720 (twice), biome clean,
build succeeds. New tests: 12 pure-logic, 2 encoder-edge, 6 component.
|
|
The Win8-Start home grid's arrangement now syncs from the backend's
home-tile-layout plugin: ONE shared document per device orientation
(wide = landscape/desktop, tall = portrait/mobile) — every landscape
device renders the same layout, every portrait device the same other
one. localStorage persistence is retired (no migration — devices start
from the shared document; the first arrangement creates it).
- adapter/tile-layout-http.ts: the injected HTTP edge (GET/PUT/DELETE
/home/tile-layout/:mode), the features/layout adapter's pattern
- tile-layout-store: server-backed, keyed by geometry mode — boot +
orientation-change probes (stale in-flight responses never land),
locally-applied edits + debounced full-document PUT with rollback to
the last server document, WS push filtered to the active orientation,
quiet ephemeral degrade when the backend is unreachable (no error UI),
sync gated while a probe is in flight
- WorkspacesHome locks the store's orientation to the resolved grid
geometry (the landscape/portrait split IS the device-type split)
- app store: the tile store lives beside the layout store; the WS
dispatch feeds 'tile-layout' broadcasts into it; 'tiles' prop from
App.svelte (the page entry owns the httpBase)
- ws adapter: the 'tile-layout' server message (parse guard) + socket
handler; wire conformance case
|
|
The relative last-activity time switched to an MM/DD date once past a week.
Now it stays relative: days continue to a month (30 days), then months
("10d", "29d", "1mo", "2mo", ...). A month ≈ 30 days; the label never
becomes an absolute date.
Verification: svelte-check 0 errors/0 warnings; vitest 1694/1694; biome
clean on touched files; build ok.
|
|
touch capture
Two blockers for scrolling the tall grid on a phone:
1. HomeTileGrid's root was `flex-1` inside a BLOCK parent (the workspace
region), so flex-1 did nothing and the root sized to its content; the
scroller's `h-full` collapsed to auto and overflow-y-auto never engaged —
extra tiles were clipped by the shell's `overflow-hidden` with no way to
scroll. The root is now `h-full`, so the scroller gets a definite height
and tall mode scrolls (wide mode was unaffected: horizontal overflow
scrolls regardless of container height).
2. `setPointerCapture` was called for TOUCH pointers at pointerdown, which
can suppress native panning in WebKit. Touch now skips explicit capture:
the browser's implicit capture already retargets the press, and after the
long-press lift the moved events bubble up to the grid container where the
handlers live — scroll gestures keep working and touch drags are intact.
Verification: svelte-check 0 errors/0 warnings; vitest 1694/1694; build ok.
Needs a human on a real phone: swipe the tall grid when tiles overflow —
it must scroll; long-press-and-drag a tile must still reorder.
|
|
The ◱ glyph next to the count was redundant decoration. The stat line is now
just "3 conv." on the left and the relative last-activity time on the
right.
Verification: svelte-check 0 errors/0 warnings; vitest 1693/1693; build ok.
|
|
grows/shrinks with the gesture
Revert the fixed append-canvas reservation (9ede1f0): pre-reserving the 3
spaces at drag start made the view jump right and created far too much free
canvas. The grid is back to hugging its content (width: max-content / auto),
so the preview extends the canvas as the tile reaches toward the edge and it
shrinks back when the gesture ends — the scrollbar moves with the drag, as
intended. The drag-target clamp (dragAppendLimit, +3 past the committed
edge) still bounds how far the preview can extend, so the "endless growth"
problem stays solved without reserving anything up front.
Dots restore the one-pitch padding extension for the append slot; the
scroll-measure effect still re-runs on drag start/end (the canvas shrinks
back on cancel). Test reverted to the content-driven assertions.
Verification: svelte-check 0 errors/0 warnings; vitest 1693/1693; biome
clean on touched files; build ok.
|
|
scrollbar change mid-drag
Previously the grid kept `width: max-content` during a drag, so the canvas
grew/shrunk as the preview placed the tile at each hovered cell — the
scrollbar resized mid-drag, making placement harder. Now the moment a drag
starts the grid's growth axis becomes a FIXED reservation: the append limit
(committed content edge + 3, span-aware) computed at drag start, so the 3
spaces "pop in" immediately, the scroller size is constant for the entire
gesture, and the unused reservation disappears when the drag ends (the
canvas returns to max-content / auto). The dragged tile's span room is
included so its body always stays inside the reserved canvas.
Also: the scroll-measure effect now tracks `dragging`, so canPan and the
scroll indicator re-measure when the reservation appears and disappears.
Test: the drag test now asserts the grid's style is a fixed px width while
dragging (never max-content) and back to max-content after the drop. (This
caught a missing `px` unit that would have silently voided the reservation.)
Update the handoff bullet to describe the reservation.
Verification: svelte-check 0 errors/0 warnings; vitest 1693/1693; biome
clean on touched files; build ok.
|
|
Dragging a tile far right grew the wide canvas endlessly (the target cell
resolved from the pointer with no upper bound), making precise placement
impossible. Add a pure `dragAppendLimit(placements, geometry, extra = 3)`:
the drag target may aim at most 3 columns (wide) / rows (tall) past the
current span-aware content edge. The bound is measured on the COMMITTED
layout — never the live preview — so it can't ratchet mid-drag; it steps
forward only when a drop commits new content. cellAt applies it on the
growth axis (wide: col, tall: row); the fixed cross-axis clamps are
unchanged. Exported via the unit index.
Verification: svelte-check 0 errors/0 warnings; vitest 1693/1693 (4 new
dragAppendLimit tests); biome clean on touched files; build ok.
|
|
The tile-organization animation used power2.inOut (eases INTO the motion
too). Switch to power3.out so the sweep starts fast and settles slowly into
the destination — the Windows-8-style snap-to-place feel.
Verification: svelte-check 0 errors/0 warnings; vitest 1689/1689; build ok.
|
|
empty-cell hover
Three drag-interaction bugs on the Win8 tile home, one shared root cause
(capture loss from DOM reordering) plus two wiring defects:
- DROP NEVER COMMITS (stuck tile): displacePlace returned the displacement
preview SORTED BY STREAM ORDER, so crossing stream positions moved the
dragged tile's DOM node in the keyed {#each} and the browser dropped its
pointer capture — pointerup never landed, phase stayed "dragging", the
follow transform never cleared. displacePlace now preserves the INPUT
render order (coordinates change only); placeAtCell re-sorts to stream
order itself, so the commit's order-renumbering semantics are unchanged.
- FOLLOW TRANSFORM OFFSET: the follow translate was raw pointer displacement,
so a grab away from the tile's top-left made the tile jump (its ORIGIN rode
the pointer). gridPointerDown records the grab offset (pointer minus the
tile's rect) and followStyle cancels the tile's true grid anchor (targetCell
origin), so the GRAB POINT tracks the pointer.
- CAN'T HOVER EMPTY CELLS: pointer handling moved from per-tile elements to
the GRID CONTAINER, which captures the pointer and is never re-parented,
never pointer-events:none, never unmounted — move/up stream to it over
empty cells and off-canvas, and capture can never be lost to a DOM move.
Tests updated: the four order-sensitive displacePlace assertions now expect
input render order (all coordinates unchanged — the plan's stream examples
still hold); HomeTileGrid drag tests still pass via event bubbling.
Verification: svelte-check 0 errors/0 warnings; vitest 1689/1689; biome clean
on touched files; vite build succeeds. Pointer capture is browser-only —
human check at the page: (1) drag a tile across columns, release — it must
drop, not stay stuck; (2) grab a tile by its corner/center — the grabbed
point must stay under the cursor; (3) hover/drag over empty cells past the
last tile — the ghost + drop must keep tracking.
|
|
Tiles can now be dropped onto specific cells; empty cells are real
estate that persists (notes/pinned-tiles-plan.md).
Data (v2): each tile carries an optional pinned anchor in a UNIVERSAL
4-row frame (tall folds its 2 columns onto frame column 1; a wide col>1
pin reads as unpinned in tall). v1 documents migrate in memory to
all-unpinned (render-identical) and write back as v2 on the next change.
sanitizeLayout drops garbage anchors.
Packing: pinned-first (first-wins on overlap, one-ply), then unpinned
tiles first-fit into the REMAINING cells — holes between pins count and
never compact. displacePlace: the user-specified semantics — the dragged
tile leaves a gap at its origin (nothing slides backward), anchors at
the drop cell, and the contiguous filled run pushes FORWARD until the
first empty cell absorbs it (X_X + Y@1 → YXX; _XZ_ + Y@2 → _YXZ).
placeAtCell commits the displacement and pins every tile; pinAtFirstEmpty
pins the create-flow tile at the first truly-empty slot;
sortByRecentActivity now also clears pins (an explicit arrangement
reset — the menu item says so).
Drag UX: pointer → cell hit-math (empty cells are drop targets); a
faint dot marks every cell center during a drag (CSS radial-gradient
overlay, one extra pitch of end padding for the append slot); the
dragged tile element itself follows the pointer (lifted via transform,
pointer-events none); a dashed tone-colored ghost marks the landing
footprint. The preview recomputes from the COMMITTED layout on every
hover-cell change (as if the user moved straight from the original to
there) while GSAP Flip animates from wherever the tiles currently are
(re-entrant — mid-flight positions captured). Escape/pointercancel
discards the preview and flies the tile home; a drop settles it onto
its new cell.
Tests: displacement (both user examples + internal moves + spans + tall
stream), pinned pack (anchors, gaps kept, first-wins, universal-frame
mapping, deleted-leaves-holes), v1→v2 migration + anchor sanitization,
store placeAtCell/no-op write guard, Escape-cancels-drag component test.
1689 green.
|
|
The home canvas now reads as a real Windows 8 Start screen instead of a
cramped, edge-hugging list:
- Geometry: the wide unit is DERIVED from the canvas height (4 rows fill
it: unit=height/5 clamped to 112-184px, gap=unit/9, margin=unit/3 clamped
24-64px) — tiles are no longer tiny, and the tile field always carries a
proportional outer margin (never compressed against the left edge). Tall
mode keeps fluid columns with a taller 148px row + 12px gap.
- Tiles take a per-workspace SATURATED tone (new pure tileTone(id): FNV-1a
hash → one of the six theme-semantic colors) with tone-content text —
the Win8 colored-tile identity. Starred is now a ★ badge (a tile keeps
its own color), not a gray-tint override.
- The tile face is the Win8 layout: small uppercase slug caption at the top,
a bold span-aware title (lg → 3xl across 1×1 → 4×2), and a single-line
stat row pinned to the bottom (count compacts to 'conv.' on 1×1 tiles,
truncation-safe so narrow tiles never clip).
- Selection is a high-contrast offset ring (visible on any tile color);
tiles lift on hover; the pan arrow is a solid elevated circle.
- Header gains a 'WORKSPACES' kicker; empty/loading states are centered
and styled for the Start-screen canvas.
Persisted data (order + spans only) and all interaction semantics
(drag/FLIP, long-press, action bar, resize picker, create popover) are
untouched. Tests: geometry derivation/clamping, tileTone determinism,
wide-vs-compact count label — 1664 green.
|
|
Rebuild the workspace home as a Windows 8 Start-screen-style tile grid,
replacing the max-w-3xl vertical card list.
Pure core (zero DOM/Svelte/fetch, fully unit-tested):
- logic/tile-layout.ts — geometryFor(width, height) resolves WIDE (4 fixed
~120px rows, column-wise flow, 4x2 span cap) or TALL (2 fixed columns,
row-wise flow, 2x2 cap); pack() is the ONLY order->coordinates transform
(greedy first-fit in fill order, hole-free, render-clamped per geometry);
firstEmptySlot/insertAtFirstEmptySlot/mergeLayout/resizeTile/moveTile/
sortByRecentActivity/sanitizeLayout. Persisted data is
orientation-independent {version:1, tiles:[{id,colSpan,rowSpan,order}]} —
order + spans only, never positions; the same data renders in either
geometry (spans clamp for the render, persisted values untouched).
- logic/drag-controller.ts — pure pointer/press state machine: mouse
threshold-drag + tap-open; touch long-press lifts + selects, pre-lift
movement cancels so vertical scrolling never fights the gesture.
Shell:
- tile-layout-store.svelte.ts — thin reactive wrapper persisting through
the injected createLocalStore adapter (dispatch.workspaces.tiles), ops:
sync/resize/move/placeFirstEmpty/sortByRecentActivity.
- ui/HomeTileGrid.svelte — CSS-grid tile canvas (wide: horizontal scroll +
snap + wheel pan + pan arrow + scroll indicator; tall: native vertical
scroll), FLIP-animated reflow, drag ghost, transient resize preview.
- ui/HomeHeader.svelte — fixed chrome: Dispatch display title, New
workspace popover (same slug validation; creates via create-on-miss and
does NOT navigate — the new tile lands in the first empty slot in fill
order and enters rename mode), global Plugins button, sort-by-recent
overflow; compact icon buttons + bottom sheet on tall screens.
- ui/WorkspaceTile.svelte — tile face (title, /slug, conversation count,
relative last-activity, live active dots, starred tint); inline rename
with the old card's semantics.
- ui/TileActionBar.svelte — slides up on select (right-click / long-press):
Open, Rename, Resize, Star/Unstar, Details, Plugins, Delete (old confirm
+ store call preserved).
- ui/ResizePicker.svelte — Word-style insert-table hover grid (wide 4x2 /
tall 2x2) with arrow/Enter/Escape support and ghost preview on commit.
- ui/WorkspaceDetails.svelte — flyout holding the old card's default-cwd
input + default-computer select (same store calls + error states).
- WorkspacesHome.svelte orchestrates; App.svelte untouched (same props).
- WorkspaceCard.svelte is removed (replaced by the tile system).
Manual tile order rules; starred workspaces get a tint (star is no longer
a sort key); workspaces unknown to the layout append at the end.
vitest: jsdom gains PointerEvent (MouseEvent-based) + Element.animate
stubs — the pointer logic is the outermost edge for the drag tests.
Verification: svelte-check 0/0, vitest 1655 passed (102 files, twice),
biome clean, vite build OK.
|
|
highlight on close
|
|
viewport, not its scrolled content
Two renderer bugs made the diagnostic layout plugin's borders useless:
1. Borders went past the panels and were invisible at screen edges: CSS
outlines draw OUTSIDE the border box by default, and tiles span their
containers edge-to-edge, so the 4px ring overflowed the visible area
and was clipped. The outline is now inset (outline-offset: -4px) so
it draws inside the tile's box.
2. The green side border scrolled with the sidebar: the marker was drawn
on the TilePanels root, which is the SCROLLED CONTENT of the split's
collapsible child. The marker now lives on the split's scroll-viewport
div — the sidebar's fixed, opaquely-painted frame: its box never moves
(only its contents scroll), and its own outline is never clipped by its
overflow. (The aside behind it can't carry the marker — the scroll
viewport's bg-base-100 paints over an outline drawn on the aside.)
markerStyle() centralizes the full declaration (outline + inset offset)
and returns undefined for unmarked/unknown markers, so unmarked trees
still render with zero outline CSS (pinned by the existing tests).
Paint layering verified with a headless-Chromium screenshot of a probe
page reproducing the exact DOM + computed styles.
|
|
The backend now activates layout providers PER WORKSPACE (the workspace's
per-workspace enablement state picks the ONE active layout plugin), so
toggling a layout plugin in the workspace plugins modal changes which tree
serves the workspace. The modal's toggle now refreshes the layout so the
newly-enabled provider's tree (or the no-provider degrade) lands without a
reload:
- WorkspaceLayoutPort gains refresh(workspaceId) — the modal calls it
after every state write (an attempted write re-probes either way: the
probe is the idempotent catch-up; a refused write changed nothing, so it
re-syncs the same tree).
- App.svelte wires the port to the layout store's new
onWorkspacePluginsChanged(workspaceId) — a guarded re-probe that only
fires for the ACTIVE workspace (another workspace's toggle is picked up
by its own probe on switch, exactly like the other §2.6 triggers).
The per-workspace tree resolution itself is backend-side; the marker
renderer, the probe triggers, and the WS push filtering are
unchanged.
Verification: typecheck (svelte-check 0 errors) / test (1528) / check
/ build all green.
|
|
|
|
pinned to bottom)
|
|
|
|
|
|
|
|
migration
|
|
|
|
boundary
PluginTable.svelte renders a keyed each ({#each groups as group
(group.tier)}); groupByTier's old "rest" bucket for unknown tiers fell
back to key "standard", producing a DUPLICATE standard group (and thus an
each_key_duplicate crash in Svelte 5, blanking both manager tables) whenever
tierless rows were present alongside canonical standard rows.
groupByTier now buckets through a Map keyed by the NORMALIZED tier — each
canonical tier can yield at most one group, so duplicate keys are impossible
by construction, and no row is ever dropped. Unknown/missing tiers fold into
the standard bucket rather than forming a separate trailing group.
The normalization (normalizeTier, in types.ts next to tierLabel/TIER_ORDER)
also runs at the pluginNodes boundary, so every node's tier is canonical
before any tier-keyed consumer sees it — including tierLabel's exhaustive
switch, which rendered an empty badge for out-of-contract values. Normalizing
into an existing bucket (option a) was chosen over a synthetic "unknown"
group because it needs no new UI vocabulary and keeps every consumer total.
Tests: all-canonical order, unknown tier ("kernel"), and missing tier each
assert no duplicate keys, no dropped rows, and the right group/row counts
from the Loaded-table view; the boundary test pins pluginNodes' canonical
output. Plugins-feature vitest green (95 tests); svelte-check, biome, and
vite build green.
|
|
|
|
|
|
save/remove identity
Draft rows (agent === null, id === null) previously keyed the other-names
exclusion and the live isNameTaken predicate by their nullable persisted id,
so a draft falsely collided with its OWN name (every name reported 'already
exists') and saving/removing one of multiple drafts could reconcile all of
them (a null agent captured identity matched every draft).
Identify rows solely by each Row's stable numeric `key` (already the
{#each} key) for: the other-names uniqueness hint, the controller's live
isNameTaken predicate, replaceAgent (save reconciliation), and removeRow.
A draft excludes itself exactly once; a persisted row keeps reconciling the
same row on later edits and no longer self-collides.
Add direct component-level regression tests (NamedAgentsView.test.ts) for
the self-collision, distinct/same-name multi-draft cases, saving-one-
updates-only-that-row, removing-one-removes-only-that-row, and post-persist
re-save reconciliation. 5 of 6 tests fail against the previous code (verified).
Verification: typecheck 0 errors, biome clean, 1252 tests pass, build ok.
|
|
semantics
Implement the frontend of the Named Agents feature (plan §5-§9):
- New features/named-agents module: pure logic (types re-exported from
@dispatch/wire + transport-contract, validation, provider/model options,
snapshot helpers, network normalizers, auto-save status model) + an injected
auto-save controller (name debounce, immediate provider/model save, monotonic
stale-response guard) + the collapsible sidebar card view.
- Store: /named-agents CRUD + boot seed; selectModel(model, snapshot?) persists
the raw model WITH the namedAgent snapshot; draft promotion carries it;
refreshModel restores ModelResponse.namedAgent.
- Chat store setNamedAgent (sends namedAgent on chat.send); Tab.snapshot +
setSnapshot (retained across rename/remove, persisted with tabs).
- Model selector: synthetic Named Agents provider ALWAYS first; selecting one
resolves to the raw model + records a snapshot; a retained snapshot of a
renamed/removed agent still displays as unavailable (plan §7).
- Composition root wires the view (viewKinds + loaded-modules manifest) + feeds
the selector.
Tests: logic + controller + selector + app-store CRUD (1246 pass), typecheck 0,
biome clean (only pre-existing config infos pending a future migrate), build OK.
|