Commit Graph
2059 Commits
Author SHA1 Message Date
Mohamed Boudra 697a0002d9 test(workspaces): isolate sibling Git fanout 2026-08-14 23:43:21 +02:00
Mohamed Boudra 1518eb09a9 test(workspaces): isolate Git watcher readiness 2026-08-14 23:26:28 +02:00
Mohamed Boudra 3175a5ff10 chore: merge main into workspace runtime branch 2026-08-14 23:10:24 +02:00
Mohamed Boudra eafa5f2b5c fix(workspaces): resolve runtime Git portably 2026-08-14 22:54:59 +02:00
Mohamed Boudra 4748aad103 Reload daemon configuration without restarting (#3365)
* feat(daemon): reload configuration without restarting

Keep config validation, reloadability classification, override handling, and atomic runtime application inside the daemon so local and remote clients share one authoritative workflow.

* fix(daemon): preserve reload resource ownership

Keep every materialized provider client under daemon-lifetime ownership and classify speech overrides from resolved providers. Restore the complete WebSocket config-store test contract exposed by CI.
2026-08-14 22:46:32 +02:00
Mohamed Boudra 12c1a68956 test(workspaces): await runtime teardown 2026-08-14 22:23:22 +02:00
Mohamed Boudra cf4d702a8e fix(workspaces): release archived runtime backing 2026-08-14 21:44:48 +02:00
Mohamed Boudra e5f3b3dc02 fix(workspaces): preserve portable runtime readiness 2026-08-14 20:13:04 +02:00
Mohamed Boudra b500809229 test: assert worktree backing release 2026-08-14 19:31:04 +02:00
Mohamed Boudra 54f0e636b9 Merge remote-tracking branch 'origin/main' into workspace-runtime-poc
# Conflicts:
#	nix/npm-deps.hash
#	package.json
#	packages/client/src/daemon-client.ts
#	packages/server/src/server/bootstrap.ts
#	packages/server/src/server/websocket-server.ts
2026-08-14 18:43:58 +02:00
Mohamed Boudra cd429339a0 fix(workspaces): preserve runtime lifecycle boundaries 2026-08-14 18:35:07 +02:00
Mohamed Boudra 23853a1515 Add managed local plugin lifecycle (#3222)
* feat(plugins): add local plugin surfaces and RPCs

Keep plugin execution scoped to its daemon while allowing one local TypeScript entry point to contribute validated RPCs and native client surfaces. Paseo owns host selection, navigation chrome, and per-installation query caches.

* feat(plugins): add Linear attachment source

* fix(nix): refresh npm dependency hash

* test(server): exclude internal sessions from Hub counts

* test(server): distinguish Hub execution sessions

* feat(plugins): make local plugins explicitly opt-in

Keep plugin loading dormant until configuration enables it, and collect unsupported examples under one directory.

* refactor(plugins): establish host boundaries

* fix(nix): refresh plugin dependency hash

* fix(plugins): complete packaging and startup rollback

* feat(plugins): add managed local lifecycle

Make trusted local plugins recoverable and authorable through daemon-owned lifecycle operations, host settings, and a capability-gated CLI. Reloads fully stop the previous plugin before starting new code, while generated projects provide strict TSX checking without changing runtime dependency ownership.

* fix(nix): refresh plugin dependency hash
2026-08-14 17:12:49 +02:00
Mohamed Boudra 51193e7a92 fix(workspaces): stabilize runtime CI paths 2026-08-14 16:49:05 +02:00
Mohamed Boudra ff83ca4bdc fix(workspaces): restore clean runtime test paths 2026-08-14 14:11:12 +02:00
Mohamed Boudra e2ac23da70 chore(merge): integrate main into workspace runtimes 2026-08-14 13:28:32 +02:00
Mohamed Boudra 7c32fcda98 refactor(workspaces): finalize runtime package boundaries
Keep the daemon dependent only on the public runtime contract and helper, while optional runtime implementations remain external command registrations. Workspace setup continues asynchronously through the bound runtime without coupling production packages to an implementation.
2026-08-14 12:48:35 +02:00
Mohamed Boudra 373a98c64d Restore host directories instantly before reconnect (#3259)
* feat(sync): restore host directories before reconnect

Persist complete project, workspace, and active-agent replicas so every registered host can paint immediately, including offline hosts. Reconcile those replicas through monotonic per-entity cursors on the existing directory RPCs without retaining an event journal.

* fix(sync): preserve active turn identity during catch-up

Own Agent-to-wire projection beside the existing snapshot normalizer so replica persistence does not reach through directory-sync internals. This also keeps identified active turns intact when unchanged agents are folded into an incremental directory response.

* test(e2e): inspect the real IndexedDB replica

Keep cache setup, reload, and hydration coverage on the production browser storage backend after the replica moved off localStorage. Measure writes at the actual IndexedDB put boundary as well.

* fix warm reconnect cache paths

* feat(sync): persist project icons and deepen directory sync

* fix(sync): preserve project update order

* fix(cache): preserve validated replica persistence

* fix(sync): retain cached agents during catch-up
2026-08-13 23:10:44 +02:00
Mohamed Boudra 36d8464068 refactor(workspaces): make Docker a generic runtime
The server build, typecheck, and dry-run package pass with the Docker implementation deleted from the tree. paseo-workspace-helper remains a mandatory generic runtime contract requirement.
2026-08-13 21:59:13 +02:00
Mohamed Boudra b44bb63cf4 chore(release): cut 0.4.0 2026-08-13 21:31:47 +02:00
Mohamed Boudra 52e4060d76 fix(server): prefer portable project icons
Project icon discovery could select an ICO that native clients cannot decode even when a renderable SVG or PNG was available. Keep ICO as the final fallback and recognize common sized icon names.
2026-08-13 19:34:37 +02:00
Mohamed Boudra a4b92768d9 fix(providers): allow two minutes for catalog refreshes
Provider catalog discovery can exceed one minute during cold startup. Increase the default deadline while preserving explicit config and environment overrides.
2026-08-13 18:52:16 +02:00
Mohamed Boudra 1a6054138d Stop idle workspace Git refreshes from starving daemon requests (#3323)
* fix(server): trust healthy workspace git watchers

Quiet observed workspaces were periodically scheduling full Git refreshes, saturating the daemon-wide process pool as workspace count grew. Keep only cheap observation re-ensure work, refresh ignore paths from watcher events, and detect silent metadata watchers with a one-shot canary before accepting the subscription.

* fix(server): refresh watcher ignores from git metadata

Repository exclude and Git configuration changes also affect the standard ignore set. Route those metadata events back to linked working-tree observations so their excluded directory lists stay current without periodic Git polling.

* test(server): drive ignore updates through watcher events

The integration contract now exercises the production event-driven ignore refresh instead of waiting for the removed periodic audit.

* fix(server): normalize git metadata paths on windows

File observation returns platform-native relative separators. Normalize those paths before matching Git metadata names so repository exclusion changes refresh working-tree ignores on Windows too.

* fix(server): trail concurrent ignore refreshes

Remember ignore-source events that arrive during an in-flight Git query and recompute once more before considering the working-tree observer current.
2026-08-13 22:08:23 +08:00
Mohamed Boudra 20bccd31a3 Make provider catalog refresh deadlines configurable (#3322)
* fix(providers): make catalog refresh deadlines configurable

Provider startup can legitimately exceed a fixed short timeout when catalog discovery has substantial initialization work. Give each provider refresh one configurable deadline covering availability and the complete catalog probe, and cancel named child operations before allowing retries so timed-out work cannot accumulate in the background.

* fix(opencode): abort catalog server acquisition

A provider refresh could expire while the helper server was still starting because acquisition only tracked the activity without observing its abort signal. Reject acquisition before transferring a server reference, while leaving the manager-owned startup reusable by the next caller.

* test(file-observer): allow native burst recovery
2026-08-13 15:08:46 +02:00
Mohamed Boudra d0d60a4052 Disconnect daemons locally when Hub is unreachable (#3321)
* fix(hub): make daemon disconnect unilateral

Local relationship removal must not depend on reaching the Hub. Fence execution authority, make one bounded courtesy revocation attempt, then clear local state so status and reconnect eligibility agree immediately.

* fix(hub): preserve disconnect intent across crashes

Persist the terminal disconnect marker only while the bounded Hub notification is in flight. Startup removes that marker, so a crash cannot restore the old relationship or create a revocation retry loop.
2026-08-13 14:00:37 +02:00
Mohamed Boudra 9ca6f24315 Let Claude apply its native MCP timeouts (#3315)
* fix(claude): preserve native MCP timeouts

Paseo forced Claude MCP startup and tool waits to ten minutes, turning unavailable servers into long silent first turns. Let Claude apply its own defaults while preserving explicit user environment overrides.

* test(claude): isolate MCP timeout regression

Exercise timeout precedence through the existing provider runtime environment input without mutating process-wide test state.

* test(workspace-git): await refresh boundaries

Synchronize fetch observation tests with initial snapshot and fetch completion before advancing fake timers.
2026-08-13 13:40:31 +02:00
Mohamed Boudra c190fed27c Keep new worktrees clean when setup changes are uncommitted (#3311)
* fix(worktrees): preserve selected base config

New worktrees were overwriting the selected ref's paseo.json with source-checkout bytes, which could create an accidental revert in an otherwise fresh workspace. Keep committed base config authoritative and warn when locally saved setup changes still need to be committed.

* fix(worktrees): seed config exclusively

* fix(projects): refresh config status on focus
2026-08-13 12:02:15 +02:00
Mohamed Boudra cbb8295ceb fix(workspaces): unblock desktop runtime lifecycle
Electron-hosted Local helpers bypassed the sanitized self-Node launch policy, so they lacked ELECTRON_RUN_AS_NODE and provider probes hung.

Command-runtime adapters lacked exact detached POSIX process-group ownership, so forced lifecycle cleanup could leave adapter descendants alive.
2026-08-13 00:46:16 +02:00
Mohamed Boudra 44b9980a70 chore(release): cut 0.4.0-beta.2 2026-08-12 23:18:28 +02:00
Mohamed Boudra 4426a6b11f Preserve opened subagents when parents are archived (#3279)
* fix(subagents): preserve opened children on parent archive

Opening a managed child represents user ownership only while that client's tab remains open. Track that intent per client, detach surviving children during parent archive, and keep single and bulk tab-close behavior consistent.

* fix(subagents): mark generic tab openings

Centralize managed child tab ownership in agent navigation so list, command-center, deep-link, notification, and subagent-track openings all mark ownership before creating the tab. Use stable localized errors for lifecycle failures.

* fix(subagents): classify cold tab openings

Fetch uncached agents before deciding whether tab ownership must be recorded, so notification, desktop, and cold-route openings cannot bypass parent archive protection.

* fix(app): translate navigation fallback errors

* fix(navigation): allow offline workspace intents

* refactor(app): derive subagent ownership from open tabs

* fix(server): serialize subagent lifecycle ownership
2026-08-12 21:18:55 +02:00
Mohamed Boudra 33afa7283b feat(workspaces): define project runtime source authority
Route probe and user materialization through one projectRuntimeSource seam, where persisted Git source overrides the host-only convenience root and must belong to Git project data.

Lock default revision, explicit revision, and subdirectory semantics with real Docker acceptance. Enforce provider-probe internals structurally and document that the current Docker POC needs a locally built image because its default reference is unpublished.
2026-08-12 20:53:05 +02:00
Mohamed Boudra 0c6dbefa40 chore(release): cut 0.4.0-beta.1 2026-08-12 20:39:35 +02:00
Mohamed Boudra 635a8be437 Restore model-default variant selection (#3281)
* fix(providers): restore implicit model variant selection

Provider catalogs list only explicit overrides, so treating the first override as the default made base model behavior unreachable. Represent the base choice in the catalog and translate it back to an omitted variant at the provider boundary.

* test(providers): exercise variant catalog boundary

The regression concerns the model catalog callers receive, so verify normalization through fetchCatalog instead of coupling the test to its internal builder.
2026-08-12 19:54:06 +02:00
Mohamed Boudra 527b252497 feat(workspaces): reconcile provider probe lifecycle
Keep lifecycle policy in provider-probe while WorkspaceRuntimeService remains the sole lifecycle executor. Idle probes pause without discarding runtime state; source and runtime fingerprints recreate probes and rotate provider bindings, while restart and project removal converge persisted records.

Close provider snapshot and helper bindings before lifecycle transitions to avoid leaks. Keep scriptTerminal in the exact bound-runtime surface because reusable local script terminals were intentionally introduced in the preceding accepted runtime slice.
2026-08-12 19:35:26 +02:00
Mohamed Boudra d03e4298ab feat(workspaces): accept Docker runtime journeys
Exercise probe, creation, terminals, file observation, Git, scripts, and agent edits through the Desktop Docker runtime while proving host decoys remain untouched.

Selected-runtime creation has a longer request budget because materialization and setup can legitimately exceed the legacy RPC ceiling; legacy creation keeps its existing timeout. Materializing records remain durable but private until final placement is published.

Scope Docker resources to the daemon owner and validate both container and volume before every lifecycle operation and creation race. Explicit Local and Worktree selection remains behaviorally unchanged, including reusable Local script terminals and the shared common-Git watcher.

Real-provider manual QA uses ignored checkout-local configuration and is intentionally not committed.
2026-08-12 18:28:06 +02:00
James Strain b58ccf9269 fix: stop completed Codex subagents appearing active (#3188) 2026-08-12 14:05:45 +02:00
Mohamed Boudra 359d5443a9 feat(workspaces): probe providers in selected runtimes
Pre-creation provider truth now comes from an invisible real runtime workspace and reuses the normal provider snapshot pipeline. Probe records are owned separately, keeping them structurally absent from ordinary workspace projections.

Setup eligibility follows fresh content materialization rather than lifecycle resource ownership. Adopted Local directories never execute repository setup, while fresh Worktree, Docker, and external content may; provider probes never execute repository setup.
2026-08-12 13:02:21 +02:00
2c5c629db8 fix(usage): read Cursor token from modern state.vscdb key via node:sqlite (#2704)
* fix(usage): read Cursor token from modern state.vscdb key via node:sqlite

Settings → Usage showed Cursor as unavailable for a normal Cursor desktop
login. The quota fetcher read the access token from the legacy `cursorAuthStatus`
key, but current Cursor builds store a plain JWT under `cursorAuth/accessToken`
with no such legacy row, so the lookup returned null.

Read the token with built-in node:sqlite instead of shelling out to a `sqlite3`
CLI (absent by default on Windows) — a second, independent cause of the same
"token not found → unavailable" failure. Prefer the modern key, fall back to the
legacy `cursorAuthStatus` blob for older builds, and log read failures at debug
so an unavailable card stays diagnosable.

Fixes #2586

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(usage): cover Cursor sqlite text and blob auth

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-08-12 12:25:04 +02:00
Mohamed Boudra 3a36c76733 feat(workspaces): choose runtime when creating workspaces
Expose the runtime catalog and carry an explicit runtime ID through workspace creation while preserving omitted-field compatibility for older clients.

The runtime POC made useGitActions require WorkspaceGitBoundary, but Command Center mounted that consumer without the boundary and crashed. Bind the active workspace at that consumer.

Keep provisional Worktree reservations unpublished until final placement so observers never bind the source checkout as the new workspace. Gate legacy cwd Git compatibility by hostVisiblePath instead of runtime kind because Local and Worktree remain host-visible runtimes.
2026-08-12 11:22:55 +02:00
Mohamed Boudra 74f2158c45 Preserve daemon sessions when workers stall (#3263)
Stop treating heartbeat silence as proof that a live daemon worker should be destroyed. Crash recovery, explicit lifecycle requests, and supervisor-loss cleanup remain unchanged.
2026-08-12 10:58:58 +02:00
Mohamed Boudra 6ca27a9b12 feat(workspaces): publish external runtime contract
Publish the rootless workspace runtime protocol, enforce module and host-execution boundaries, and prove an independently authored command runtime across lifecycle, PTY, files, Git, providers, and CI.
2026-08-11 22:14:47 +02:00
Mohamed Boudra 40bdb9a043 fix(server): keep daemon workers alive across host sleep (#3235)
The worker watchdog treated wall-clock time spent in host sleep as evidence that the worker was unresponsive. Count consecutive observed heartbeat misses instead so sleep does not destroy live terminal sessions while genuinely stalled workers still restart after the same active observation window.
2026-08-11 21:48:06 +02:00
Mohamed Boudra 2f54c0e309 Use saved Project Settings for new worktree setup (#3233)
* fix(worktrees): honor saved setup configuration

Project Settings writes paseo.json in the source checkout, but existing committed copies in new worktrees previously won silently. Copy the saved project config before lifecycle setup so the command shown to users is the command that runs.

* fix(worktrees): replace config symlinks safely

A selected ref can place a symlink at paseo.json, so copying over that path may overwrite a file outside the worktree. Materialize the saved source config with atomic rename semantics to replace the path entry without following it.
2026-08-11 21:20:30 +02:00
163e7d1cc4 fix(omp): negotiate RPC protocol v2 so large model catalogs don't overflow (#3184)
* fix(omp): negotiate RPC protocol v2 so large responses don't overflow

OMP caps protocol-v1 single-line RPC frames at 1 MiB. get_available_models
returns the full effective model catalog (hundreds of models), which exceeds
that cap, so OMP replies with an overflow error ("RPC response exceeded the
transport limit") and the OMP provider fails to start sessions.

Negotiate RPC protocol v2 after OMP's startup `ready` frame when it is
supported: v2 lifts the single-frame ceiling to 64 MiB by chunking oversized
frames. Add rpc_chunk reassembly to the shared JSONL transport so chunked
responses are reconstructed before routing, and gate the negotiation on the
ready frame so it is in place before the first request.

Protocol v2 is negotiated only when the peer advertises support in its ready
frame; peers that stay on v1 (e.g. Pi's --mode rpc) are unaffected.

* fix(omp): make protocol startup atomic

---------

Co-authored-by: pi3123 <pi3123@users.noreply.github.com>
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-08-11 19:19:24 +02:00
Hanjun(Tony)andThong Van cdb1163145 Notify callers when watched children close and cap responses (#3192)
Wake caller agents when watched children close and cap embedded child responses at 4,000 characters.

Co-authored-by: Hanjun(Tony) <54878785+wilgon456@users.noreply.github.com>
Co-authored-by: Thong Van <minhthong@gmail.com>
2026-08-11 19:08:41 +02:00
Edi HasajandMohamed Boudra f24664b882 fix: terminalize Codex compaction on turn end (#3211)
* fix: terminalize Codex compaction on turn end

* fix: ignore stale Codex compaction completions

---------

Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
2026-08-11 19:02:17 +02:00
Mohamed Boudra db6520bf36 Show live task progress while agents work (#3227)
* feat(agents): surface live task progress

Keep current task state above the composer and turn task snapshots into semantic timeline activity. Normalize provider task protocols while preserving genuine plan flows.

* fix(agents): admit tasks through stream sequencing

* test(agents): expect normalized task status
2026-08-11 18:23:30 +02:00
Mohamed Boudra b2ff359b60 Always create a fresh worktree (#3224)
* fix(server): always create requested worktrees

A slug is a preferred name, not workspace identity. Let the worktree creator suffix occupied paths and branches so duplicate and detached-slug requests still create fresh worktrees.

* fix(server): suffix occupied checkout branches

Fresh creation also applies when checkout targets are already in use. Create a suffixed branch from the requested branch instead of rejecting the request.

* test(server): align occupied branch coverage

* test(server): remove stale worktree reuse expectations
2026-08-11 17:54:59 +02:00
Mohamed Boudra 308e9af845 Add reusable agent profiles (#3208)
* Search models across all providers from the picker root

* Store agent profiles in daemon config and apply agent config in one RPC

An agent profile is a named bundle of provider, model, mode, thinking
option, feature values and notes, stored host-wide next to terminal
profiles. It carries no system prompt: AgentSessionConfig.systemPrompt is
creation-only, so a profile holding one would apply when starting an agent
and silently do nothing when applied to a running one.

Applying a profile through the four single-field RPCs can fail halfway and
leave an agent matching no profile, so agent.config.apply takes the whole
bundle. Model runs first because a thinking option only means anything
against the model that offers it.

Old daemons parse their persisted config strictly and would drop an
unknown agentProfiles key, making a save appear to succeed and vanish, so
both capabilities are advertised on server_info for the client to gate on.

list_profiles exposes the profiles to orchestrating agents, which read the
notes to pick one and copy its values into create_agent.

* Add the agent profiles settings section

Profiles live behind one module boundary so removing the feature is a
directory plus one mount, which is what favourites never was. The picker
and composer will ask for rows and an apply function rather than learning
what a profile contains.

The edit form follows the form-model pattern instead of the terminal
profile modal's per-field state, because provider selection cascades into
which models, modes, thinking levels and features exist below it.

The section is gated on server_info.features.agentProfiles: an older daemon
drops the key on write, and a save that appears to work and then vanishes
is worse than an unavailable state.

* Document agent profiles and name the four senses of profile

The glossary reserved Profile for HostProfile, terminal profiles owned the
user-facing word, and custom-providers.md used it for provider aliases.
Agent profile is now the term, with all four cross-referenced.

data-model.md never documented terminalProfiles either, so both profile
lists are backfilled together along with the whole-list patch semantics and
why absent and empty differ.

* Pin agent profiles to the model picker and delete favourites

Applying a profile writes its values into the agent controls and is then
forgotten, so rows carry no checkmark and no selected state: after applying,
nothing is selected. Chevron still means navigate deeper, so the absence of
one is what marks a row as an action.

A running agent is its provider's process and agent.config.apply carries no
provider, so the live picker only offers profiles matching the agent's
provider rather than showing rows that cannot do what they say. The draft
path has no such limit and switches provider freely.

Draft feature values go through preferences rather than the feature
callback alone, because the draft feature hook clears local values whenever
the provider changes and a provider-switching profile would lose them.

Favourites are deleted rather than migrated: they were device-local
AsyncStorage keyed by provider and model, while profiles are host-wide
daemon config. ProviderSelectionModelRow.favoriteKey stays as row identity
and moves to its real owner; renaming it is its own commit.

* Cover agent profiles and cross-provider search with E2E specs

Model rows expressed selection only as a checkmark glyph, which assistive
tech cannot see and a spec cannot assert. Rows that draw the indicator now
carry aria-selected; profile rows deliberately carry none, because a
profile is an action rather than a selection. That is what makes "nothing
is marked selected after applying a profile" a real assertion instead of a
tautology, with the materialized model one level down as the control.

The search fixture extends claude rather than mock: provider-config.ts
only allows the six shipped providers as extends targets, so a derived
provider cannot inherit the dev-only mock provider.

* State what applying a config bundle does on partial failure

applyBundle is not a transaction and the earlier commit message implied it
was. A step that rejects aborts the rest and the steps that already applied
stay applied, which a profile omitting modelId can reach: provider setters
validate against the agent's live model, so mode can land while a feature
that model does not support is rejected.

Nothing is hidden by that. Every setter emits agent state before returning,
so what applied is streamed to the client next to the rejected response.
Rolling back would re-invoke the setters that just failed, usually against
a session that is gone, and pre-flight validation would restate each
provider's model-dependent checks where they would drift from the setters
that own them.

What sending the bundle in one request removes is the multi-round-trip
failure class: interruption between hops, and interleaving with another
client. The test already pinned this behaviour; its name now says so.

* feat(agent-profiles): refine profile discovery and appearance

Make profiles discoverable from empty and provider-scoped model pickers, keep global search stable and virtualized, and move profile settings under Agents. Replace emoji identity with a shared Lucide icon and host-color picker, while requiring concrete provider selections so profiles materialize predictably.

* fix(agent-profiles): defer profile swatch styles

Resolving Unistyles proxies at module load can freeze the temporary startup color scheme before the persisted theme is restored. Keep only style-key names at module scope and resolve the selected swatch during render.
2026-08-11 17:42:35 +02:00
Mohamed Boudra cbf59f4c96 feat(workspaces): own lifecycle through runtimes
Reconcile runtime placement and lifecycle by workspace identity, preserve state through archive and recovery, and make deletion, observation replay, scripts, and resource cleanup convergent across local, worktree, and Docker runtimes.
2026-08-11 16:49:27 +02:00
Mohamed Boudra 36a13601dd Let users choose the metadata generation model (#3215)
Expose Automatic and Manual metadata model selection in Host Settings, preserve existing configured fallbacks, and keep structured generation resilient through discovered candidates. Includes Playwright coverage, translations, public documentation, and settings-row alignment polish.
2026-08-11 14:01:00 +02:00