Conflicts resolved against #604, #606 and #601:
- constants.js: the rooted link pattern takes html-md's `markdown` and
`html?` with #606's m4v/ogv; #604's relative pattern gets the same list
(link-provider-regex pins the two equal); #604's findFilePathLinks /
folder helpers and html-md's rendersAsPage() are both kept, and
window.CodemanFilePaths exports all of them.
- terminal-ui.js: #604's resolved `target` (relative, ~/ and folder links)
now also goes through rendersAsPage(), so every link kind routes .html to
the page preview; .md routes there through FILE_PREVIEW_EXTENSIONS.
- CLAUDE.md, api-reference, architecture-invariants: both sides' text kept.
- test/folder-grid-ui.test.ts (#604) stubs rendersAsPage() like the other
panels-ui harnesses.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Conflicts resolved against #605 (and #602's test mock):
- CLAUDE.md Prompt uploads: #606's rewrite is kept and #605's bounded
workspace probe sentence follows the remote (SSH) refusal.
- test/routes/session-routes.test.ts: the remote-pane-history and
bounded-path-probe mocks are kept beside #606's _resetPasteRateBuckets
import, and both PRs' paste-image tests are kept.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Conflict resolved in docs/api-reference.md: #607's prompt cache section and
#528's tabGroupId section are both kept.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Conflict resolved in terminal-tile.js's @dependency header: #601's
SHELL_LOAD_BASE_QUERY/SHELL_LOAD_QUERY and #527's pageKeysForGesture and
app.tabAlerts are all listed.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Codex starts stdio MCP servers with a minimal environment, so `codeman mcp`
under the documented Codex config never learned the session it runs in: the
self-protection and the children rule did not apply. Both Codex TOML snippets
now forward the two variables with `env_vars`.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Owner decision 2026-10-13 (#547): a rendered .html page behaves like a
file opened from disk in a browser. It may load scripts, styles, fonts,
images and media from any host and make fetch, XHR, WebSocket, beacon
and form requests to any host.
The /html-view Content-Security-Policy is therefore the `sandbox`
directive alone (HTML_VIEW_CSP, now exported and documented): the same
sandbox flags as before, never `allow-same-origin`, and no fetch
directive. It replaces Codeman's own `default-src 'self'` policy on these
responses. Everything that confines a page stays: the opaque origin (no
Codeman cookies or document, API writes with `Origin: null` refused, API
reads not readable cross-origin), and the capability serving only the
page's own folder under every path, symlink, dot, extension and
confinement rule.
A new test builds the route behind registerSecurityHeaders, as in
production, and pins the exact header: one `sandbox` directive with the
current flags, no `allow-same-origin`, no `-src` directive. The security
architecture, architecture invariants, API reference, CLAUDE.md and the
Working With Files wiki page now say what a page can do (reach the
network, read its own folder) and cannot do (Codeman's origin, cookies,
API, other folders), and that this is the owner's decision.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
An in-workspace .md path clicked in the terminal went to the log viewer
(tail -f, a "streaming" header over raw text), because md was kept out
of FILE_PREVIEW_EXTENSIONS on purpose; every other surface already
rendered it. Owner decision 2026-10-13: markdown is a document to read,
so md and markdown join FILE_PREVIEW_EXTENSIONS and a click or tap opens
the file preview, rendered by default with the MD pill for source. The
main pane, every grid tile and the split's Pane B share the link
provider, so all of them route the same way. Other in-workspace text
(.log, .txt, .json, code) keeps the log viewer and its live follow.
FILE_PATH_LINK_PATTERN now also links `.markdown` and `.htm`, which
were not clickable at all.
Tests: file-preview-markdown.test.ts now asserts md and markdown ARE
routed to the preview (and plain text is not); terminal-touch-tap pins a
tapped .md, .markdown and .html opening the preview while .log, .txt and
.json open the log viewer, and a tile-registered provider routing the
same way against the tile's own session. Docs: CLAUDE.md, the
architecture invariants and the wiki (Working With Files, Mobile Guide).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Conflicts resolved:
- CLAUDE.md: kept master's Prompt uploads paragraph and its longer
File-path links paragraph (XLSX admission limits), and inserted the
PR's html-view sentence after the download-only rule.
- src/web/public/terminal-ui.js: kept master's tile-aware
getSessionId() in the shared file-path link provider and added the
PR's .html routing to the file preview on top of it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Agents pick tools from MCP clients and registries, and Codeman had no MCP
surface: an assistant in Claude Desktop, Cursor, Codex or Gemini CLI could
not start or drive Codeman sessions without hand-written HTTP calls.
`codeman mcp` is a stdio MCP server and a CLIENT of the existing HTTP API
(it reuses codeman agent's transport and guards; no new route). Seven
tools: list_sessions, start_session, send_prompt, wait_for_session,
read_output, interrupt_session, delete_session.
- Dual-era: answers the legacy initialize handshake (2024-11-05 to
2025-11-25) and the 2026-07-28 revision (per-request _meta,
server/discover, resultType, and the ttlMs/cacheScope hints a modern
tools/list must carry: Claude Code 2.1.296 drops every tool without
them).
- Never guesses a server: the URL comes from --url or CODEMAN_API_URL.
Unconfigured, it still lists its tools and every call says how to
configure it.
- Prompting, interrupting and deleting are limited to sessions the process
started (or, inside a Codeman pane, that session's own workers) unless
--allow-any-session; never its own session. Control bytes are refused
before any request.
- Waits default to 45 s (Codex's MCP tool timeout is 60 s), max 600 s.
- read_output returns text only: a client that prefers structuredContent
otherwise showed the model metadata instead of the output.
- config/mcp-registry/server.json and package.json mcpName prepare the
official MCP registry listing; sync-plugin.mjs keeps its version.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The agent skill only reached Claude Code: injection wrote
.claude/skills/codeman, which no other CLI reads, and the per-session
preamble was seeded for claude alone. A Codex, Gemini, OpenCode,
Antigravity, Grok, DeepSeek or OMP session inside Codeman never learned it
could start and drive workers.
- Registry field capabilities.agentSkillDir (a closed enum, since
clis.json can set it and it decides where the server writes) names the
skills directory each CLI scans. Claude keeps .claude/skills; those seven
CLIs get the same skill in <case>/.agents/skills/codeman (each measured to
scan it; Claude Code does not, so it never sees two copies). pi gets none:
a project skill makes pi ask "Trust project folder?" at startup.
- The preamble is seeded for every local session whose CLI takes the
skill; nothing in it is claude-specific.
- The skill text is CLI-neutral and names the two CLI-side failures to
report instead of retrying: a sandbox that blocks loopback (Codex's
default), and commands that do not carry this session's environment
(Codex 0.162 runs them in a shared app-server daemon).
- POST /api/sessions injects after the launch pre-flight, so a CLI that
cannot start leaves nothing in the workspace.
- codeman skill install gains --mode; uninstall --case removes the copy
from every directory the registry can write.
- The settings toggle moves to Agents & CLIs > All agent CLIs.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude Code's tracker nulls recache_tokens_if_cold at a compaction or a tool-result clearing and only counts the expected rebuild at the next request, which also closes the window, so no report carried both and the old rule never fired. Stamp on the first warm null report after one that carried a value, carry it through idle re-reports and drop it at the next request; the readout now says "Context compacted at HH:MM" since a clearing opens the same window.
Also persist a changed report so a crash restores the latest one, rename the session field to statusLineExporter (statusLineTelemetry is the registry capability) and document its values, word the stake as re-cached, move the readout block above the SSE staleness heading, and drop keep-warm mentions until it lands.
Review of #527 (Ark0N, 2026-10-11):
1. A slow touch drag paged a full screen per line. touchmove calls the
fallback once per whole cell, so a slow drag's calls were further apart
than the 150 ms gesture gap and each pre-paid a page (12 lines at 170 ms
sent 12 PageUps). The touch paths now tag every call, touchmove and
momentum frames alike, with a per-contact touchId (incremented on
touchstart), and pageKeysForGesture never splits a contact by time; a
new contact is a new gesture.
2. A pre-paid debt now survives up to PAGE_KEY_DEBT_MS (1 s) of quiet when
the next event goes the same way, so slow small wheel ticks (40 px every
250 ms, a Firefox 1-line notch) pay it back instead of each pre-paying.
A reversal inside the gap (or inside one contact) drops the debt and
accumulates without pre-paying, so a +/-3 px wobble no longer pages
back and forth. A reversal after the gap still pages at once. Main pane
and tiles share the helper, so both get it.
3. A wheel consumed by the dialog check now logs page-keys-dialog-open.
4. The Dashboard wiki names newer Codex beside OpenCode; the invariants
paragraph describes the new gesture boundaries.
Tests: unit regressions through the real _maybePageCliTranscript, helper
cases, tile/main parity for ticks and wobble, and a Chromium test that
drives the real initTerminal touch listeners (12 PageUps before, 1 after).
One conflict, in i18n.js SKIP_SELECTOR: master added '.tab-name' itself
(the same fix this branch made for zh-CN tab names), so master's line is
kept and this branch's duplicate comment dropped.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A session or web-tab ref in the owner tab layout may now carry
`focus: true`. "Add to Focus" in a row's menu (right-click on a web tab)
sets it; the vertical rail then draws a shortcut to that tab in a pinned,
collapsible "Focus" section at its top, while the row itself stays where
it is. A click on a shortcut selects its tab, a right-click or the menu
key offers "Remove from Focus".
Model: `TabRef.focus?: true`, parsed strictly (only `true` or absent) and
carried through normalization, every pure edit and the legacy-order
projection; `setTabFocus()` is the pure helper. TabLayoutService needed
no change and no route was added: the flag rides the existing
`PUT /api/tab-layout`. Browser: `copyRef`/`normalizeLayout` keep `focus`
the way they keep `placement` (the browser writes whole layouts, so a
dropped field would be erased on every save), `setFocus` is a named
operation through `createEditCoordinator` and replays on a 409, and
`focusRefs()` (also on `project()`) lists what the rail can draw.
The section is its own element in #tabRail above #sessionTabs, so it
works on the flat rail (no named groups) as well as the grouped one, and
shortcuts are never rows: no Alt+N badge, no tree item, no lineage
anchor, no drag target. It is a list with one tab stop and
Up/Down/Home/End between shortcuts. Collapse is per-device localStorage
under its own key. A shortcut whose session closed or whose web tab is
not open drops out; the server already prunes deleted refs. zh-CN
strings added.
A phone-only toolbar Attach button hands the agent a photo or a video with the
keyboard down. It owns its own file input, positioned over the button so iOS
anchors the picker popover to it; the picked paths land on the prompt as unsent
text, so Compose adopts them together with anything already typed and Use
terminal keyboard hands a single-line draft back the other way.
Videos take a second path on the same route: ?kind=video streams the file to
disk under MAX_PASTE_VIDEO_BYTES (1GB, env CODEMAN_MAX_PASTE_VIDEO_BYTES)
instead of buffering it, sniffs the container from the first bytes as they
arrive, drains a mislabeled body unwritten and unlinks on any failure. It draws
on its own 3/min per-(IP, session) rate bucket beside the image path's 30/min:
at 1GB each, 30/min would be 30GB of disk a minute. Without the flag the route
is byte-identical to the image-only version, same 50MB cap and same bucket.
One client classifier, _promptAttachKind (MIME first, then the name's
extension), drives every pick and paste surface: the Attach button, terminal
drop, the tile grid's drop, the Compose and paste-dialog pickers and the
clipboard handlers. A pick that lands while the tile grid owns the terminal
goes to the PTY rather than the parked main-terminal overlay.
Claude Code's statusline has carried a prompt_cache object since v2.1.251, and Codeman's statusline exporter already received it, but StatusTelemetrySchema stripped the whole object, so Codeman never knew whether a session's conversation was still cached. Keep it and show it.
StatusTelemetrySchema now lists every documented scalar field null-tolerant (several are documented as possibly null, and the object gains fields across CLI versions, so a strict shape would 400 the post and blank the footer), while parsePromptCache normalizes only the subset Codeman stores, so a hostile localhost payload cannot bloat state. Session.promptCache rides toState with a change-only session:promptCache event, and the route broadcasts the stored object.
The Session Options (Respawn tab) readout shows warm-until or cold with the re-read stake, the cache hit rate, the last miss's causes, and, for Claude Code's own idle compaction (recache_tokens_if_cold null while expected_rebuilds rose), a "compacted at HH:MM" note that is never raised as an alert. setPromptCache stamps that on a new object rather than mutating the parsed report, so the stamp reaches the broadcast. Sessions without a statusline of their own also get a cache:until HH:MM / cache:cold group in the terminal footer. The last report is restored on a Codeman restart (like displayModel) so the readout is not blank until the pane's next render; the readout is time-aware, so a value whose expiry has passed reads as cold, not stale-warm. A launch without the exporter records why (collection off, workspace statusline, remote) so the row says the cause instead of "not reported yet" forever.
This is the sensor a later opt-in keep-warm would read, and the TTL it reports is the source of truth rather than the plan tier.
The wiki said a session closed in the meantime frees its place for the
ranking. A session popped out into its own window does the same (it is
never tiled while its window owns its PTY size, and since the previous fix
popping it out with the grid open keeps the count, too), so the sentence now
names both.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The wiki, decision 11 in the tile-grid plan, its Persistence section, the
layout-memory test header and two tile-grid.js comments said the Tiles
button and a page reload bring the grid back however it was closed. Only
the Tiles button does: a grid closed before the reload stays stored as
open: false, the page shows the single view, and the toggle puts the grid
back. The behaviour was right; the wording now says that.
A test pins it through the real close path: arrange the grid, close it with
the toggle, start a fresh page, and the restore leaves the single view
while the toggle still brings back the cells, count, divider sizes, focus
and zoom. ('a stored closed grid leaves the single view' in
tile-grid-restore.test.ts covers the same from a raw stored value through
handleInit.)
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A page reload puts a stored open grid back inside handleInit and fills a
cell freed since (its session deleted while the grid was closed) from the
ranking. Pending approvals, the source of the needs-input group, reach the
page only through seedApprovals, an async GET that lands after the restore,
so for that one fill a session waiting on a permission dialog or an unseen
finished turn ranks with the quiet ones (working sessions still rank first).
Holding the fill back until the seed lands would open fewer tiles, which
can be another shape, and then reshape the grid and move the user's tiles a
moment after the reload, so a late seed never re-forms a restored grid. The
plan, architecture-invariants#tile-grid and the wiki now say so, and a test
pins it: the seed arrives after the restore, the ranking then puts the
needs-input session first, and the restored cells and the stored layout
stay as they came back.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
After a session's first load per page, a tab switch fetched a 1 MiB tail of
the server's byte stream. For a fullscreen claude pane (alternate screen,
tmux `history_size` 0) that tail is old repaints of one frame, and an idle
tab parsed it twice (the cached copy, then the fresh one).
Every terminal response now records `paneHistoryLines` per session, and
`selectSession` takes `full=1` whenever the last report was 0. For such a
pane that capture is the visible frame, a few KB, which is also what a page
load already shows. A response without the field forgets the entry, so
unknown never counts as empty; a pane that starts keeping history goes back
to the tail on the next switch. Shells and the refresh path are unchanged.
Measured with real prod payloads through the real client in chromium:
median switch 1.26-1.66 s -> 0.10-0.19 s, parse ~1.1-1.5 s -> 2-3 ms.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The tile-grid plan records owner decision 11 (the layout is kept per
browser and comes back exactly; a fresh or filling grid ranks working,
then needing input, then most recent) and marks where it supersedes
decision 10's "the count wins over a remembered grid's size". The plan,
architecture-invariants#tile-grid and the CLAUDE.md Tile grid paragraph
now describe the stored format (`count`, freed cells, still v: 1), every
close path keeping it, and the open order; the wiki tells users what the
Tiles button brings back and how a new grid is chosen.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The "Showing the most recent 1.0 MB of this session. 4.8 MB more may still
be retained." bar covered the top of the terminal on nearly every tab switch,
and for fullscreen claude panes it could never be acted on: `truncated`
measures the server's byte stream, while Load full history asks tmux, which
holds no scrollback for a pane that lives in the alternate screen.
- The capture reads `#{history_size}` in the cursor query it already makes
and the terminal route returns it as `paneHistoryLines`.
- A reported 0 hides the notice; a positive count replaces the byte gap in
the message.
- The notice is lazy: it appears once a wheel/touch gesture reaches the top
of the browser buffer (after the pull that gesture starts), leaves when
the user scrolls back to live output or switches tabs, and its dismissal
sticks per session until reload.
- formatHistoryBytes no longer prints "1024 KB" just under 1 MiB.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
183efacc turned drag off while the rail search narrows the list, as the
owner asked, by refusing the flat manual rail's dragstart. That also
refused a drag that never reorders anything: with the tile grid open, a
found tab dragged onto a tile or an empty cell set no draggedTabId, so
_acceptTabDrops() (tile-grid.js) saw nothing and the drop was lost. On
the PR head that drop replaced or swapped the tile. The owner's reason
covers the reorder only: a reorder drop is saved for every device,
relative to rows the search hides, while the tile grid is per-device and
lands beside nothing hidden.
dragstart is unconditional again. The rows refuse the reorder instead:
their dragover returns before preventDefault while _tabRailSearchActive()
(the browser shows no-drop and fires no drop there), and their drop
returns before touching sessionOrder, for anything above the row that
might let a drop through. `draggable` stays on, so a cleared search
reorders with the same rows. The grouped rail's pointer drag keeps its
refusal in _onTabLayoutPointerDown(): it has no drop target besides the
rail.
Test: during a search that leaves both rows showing, dragstart is not
cancelled and sets draggedTabId, dragover on another row is not
accepted, a drop there leaves sessionOrder alone and saves nothing, the
real _acceptTabDrops() binder accepts the drag on a tile and hands it
the id, and after clearing the same rows reorder. It fails on the
previous commit, without the dragover guard, and without the drop guard.
CLAUDE.md, the invariants, the Dashboard wiki row and the release
changeset now say reordering by drag is off and a tile drop still works.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
From the owner's review of #580 rather than the bot's report: "Drag
while searching: I'd turn it off. A drop is saved for every device
(session order or the tab layout), and where it lands relative to the
rows the search is hiding is something you only see after clearing it."
The PR head left drag on.
Both rail drags now refuse while the search narrows the list: the
grouped rail's pointer drag in _onTabLayoutPointerDown(), and the flat
manual rail's HTML5 drag in its dragstart listener. The flat rail is
refused in the listener, not by flipping `draggable`, because a
keystroke in the box does not re-render the rows, so a cleared search
drags again with the same rows. The sidebar filter box, the header strip
and the keyboard moves (Ctrl+Shift+{ }, the row menu) are unchanged.
Tests: a grouped-rail press during a search starts no drag and one after
clearing does; a flat-rail dragstart during a search is refused and one
after clearing goes through. CLAUDE.md and the Dashboard wiki row say
so.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Conflict in docs/wiki/Settings-Reference.md resolved by keeping #565's Apply
wording and adding Copilot (and COPILOT_HOME, which the bot's review found
missing from this list).
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A session with a tab alert (red action or yellow idle, whatever tabAlerts
holds, the same set a collapsed group header surfaces) now stays visible
while the rail search or the sidebar filter is narrowing the list, even
when its name does not match. A prompt waiting on you should never be
hidden by a view filter.
The pure CodemanTabSearch.filter decides it: a row passed with keep: true
is never hidden. It counts toward its group, so the group stays on screen
and the header number is the rows left showing, but not toward
matchCount, so "No sessions match" still shows above a lone alerted row.
_applyTabListFilter() flags session rows from tabAlerts; web tabs carry
no alerts and are never kept.
No new wiring: updateTabAlertFromHooks() and _onSessionWorking() already
call renderSessionTabs(), and both render paths end in the shared filter.
The README and wiki tour image still showed the 1.7.0 UI. The new one is
a live capture of 1.40.0 (compact header pills with the plan-usage chip
beside them, File Viewer and Tiles buttons, tab logos, Run CC) with the
same three callouts: session tabs, live plan usage, one-click Run.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A "Search sessions" box at the top of the vertical tab rail narrows the
list to the tabs whose name matches (case-insensitive substring; a web
tab matches by its title). It searches every group, collapsed ones
included: while a search runs the projection draws every group open and
the header will not toggle, and the stored per-device collapse state is
left alone. Groups with no match hide, an empty result says "No sessions
match", and the flat rail (no groups) filters the same way. Escape or
the clear button empties it; leaving the vertical orientation resets it.
It is a view filter only: rows get the same tab-filtered-out class the
sidebar filter box uses, through one shared _applyTabListFilter() over
the pure CodemanTabSearch matcher in constants.js. Grouping, order,
Alt+N badges and drag are untouched, nothing is persisted or sent to
the server. The sidebar keeps matching name plus working directory.
In the grouped tree, hidden rows and the headers of emptied groups leave
the roving walk and posinset/setsize, and the tab stop moves onto a
visible item. zh-CN strings added.
Review fixes for "place new, child and web tabs into groups".
- Spawned tabs follow their parent only with the new synced setting
spawnedTabsFollowParent (App Settings > Appearance > Tabs, off by default).
TabLayoutService.sessionCreated() reads it fresh through a
childrenFollowParent dep: off, a spawned session lands after its parent and
is marked manual in the same write; on, it follows until moved by hand. The
row menu shows the placement note and "Follow parent again" only when on.
"New session" in a group menu is unaffected. zh-CN label and a
Settings-Reference row.
- applyLegacySessionRank marks a child manual only when the requested order
splits it from its parent's block. A PUT of the current order, or one that
moves a family together, leaves every stored placement alone.
- The browser never echoes a create or delete through PUT /api/session-order,
with or without groups: the layout service already re-projects the order and
broadcasts it, and syncSessionOrder adopts it.
- The placement note in the row menu is plain text described by the menu, not
a focusable menu item.
- A tab that first appears inside a collapsed group briefly marks that group's
header.
- i18n skips .tab-name, so tab names are never translated.
- Tests: legacy-order cases for an unchanged order, a family reorder and a
split; service cases for the setting and for order PUTs; the scheduled
registration test now exercises register/undo; the grouped-create test binds
port 0; one route case runs through the real layout service.
Every animation setting now has its own App Settings section, right after
Appearance (owner: easier to find). It holds the Entrance Theme (the former
Entrance Animations row), Tile Animations, and an Open lab button that closes
settings and opens the per-surface lab (?animlab=1). Appearance keeps the skin,
identity and tab settings. New animation settings go in this section;
test/app-settings-structure.test.ts pins it.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Tiles become the fifth entrance surface (data-tile-anim), switched on in App
Settings > Appearance > Tile Animations and OFF by default: the default is the
grid's own quick fade (`settle`), exactly as before.
A styled tile plays in two beats: its frame enters as it mounts (fly out of
its session tab, dealt from the Tiles button, CRT power-on, beam down from its
tab, cascade, pop, soft), and its screen then plays the terminal pane style of
the entrance theme when its first capture lands, through the same
html[data-term-anim] rules on .tile-body. Each style has its own exit when the
Tiles button closes the grid (back into the tabs, a CRT switch-off, ...).
Picking an Entrance Animations theme presets the tile style (new Launch theme:
tiles fly from the tabs); the theme readout ignores the tile row, so changing
it never shows "Custom". Frames move transform and opacity only (one fit and
one PTY resize per tile), a reload's restore always settles, and nothing moves
under reduced motion. The lab (?animlab=1) gets a Tile grid group, a cascade
order picker and in-place replay / close + reopen.
Also removes the terminal pane's `boot` entrance style (owner decision); a
saved `boot` falls back to off.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>