Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 14 additions & 2 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ multi-PR refactor plan for the reasoning behind each boundary.
execution, WebSocket/MCP hosting. No UI types. No background work started from
constructors.
- **App** (`App.xaml.cs`): composition root and top-level lifecycle only.
- **Shared mutable domains**: one observable service/store owns each persisted
domain. View models consume snapshots and field-scoped or compare-and-swap
mutations; they never own backing files, concrete managers, file observers, or
parallel mutable caches.

## Single-source owners

Expand All @@ -57,10 +61,13 @@ These are the canonical homes. Do not reintroduce private copies elsewhere.
| UI-thread marshaling for presentation code | `IUiDispatcher` | authoritative |
| Page view-model activation/deactivation + disposal lifetime | `NavigationScopeManager` | authoritative |
| Presentation-layer DI composition root | `AppServiceRegistration` (root `ServiceProvider`, owned by `App`) | authoritative |
| Settings snapshot read + batched save + non-echoing change notification | `ISettingsStore` | authoritative |
| Settings snapshot read + field-scoped save + origin-aware change notification | `ISettingsStore` | authoritative |
| V2 exec-approvals snapshot/CAS persistence + observation | `ExecApprovalsStore` through `IExecApprovalsPresentationStore` | authoritative |
| Settings page load/persist view logic | `SettingsPageViewModel` | authoritative |
| Managed-local listener provenance and strong-credential authorization | `ManagedLocalGatewayPortProvenanceService` | authoritative |
| Managed-local automatic repair eligibility and orchestration | `ManagedLocalGatewayAutoRepairMonitor` + `ManagedLocalGatewayRepairCoordinator` | authoritative |
| Permissions page state, settings commands, and exec-approvals presentation | `PermissionsPageViewModel` | authoritative |
| Permissions runtime status projection | `PermissionsPageRuntimeSource` | authoritative |
| Capability UI metadata | `NodeCapabilityUiCatalog` (planned) | planned |
| Capability registration/gating | `NodeCapabilityRegistrationPolicy` (planned) | planned |
| Local MCP exposure policy | `McpCapabilityPolicy` (planned) | planned |
Expand All @@ -77,6 +84,7 @@ These are the canonical homes. Do not reintroduce private copies elsewhere.
| `src/OpenClaw.Tray.WinUI/Chat/OpenClawComposer.cs` | `ComposerViewModel`, `SlashCommandPalette`, `AttachmentPreviewStrip`, `VoiceComposerController` |
| `src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml.cs` | `ConnectionPagePlan` (pure), `ConnectionPageViewModel`, gateway row models |
| `src/OpenClaw.Tray.WinUI/Pages/SettingsPage.xaml.cs` | settings read/persist → `SettingsPageViewModel` + `ISettingsStore`; keep gateway-uninstall, uptime timer, saved-indicator, and app-info in the view |
| `src/OpenClaw.Tray.WinUI/Pages/PermissionsPage.xaml.cs` | state/commands → `PermissionsPageViewModel`; runtime projection → `PermissionsPageRuntimeSource`; persistence → `ISettingsStore` and `IExecApprovalsPresentationStore`; keep exact WinUI rendering, clipboard/privacy actions, and save-hint timer in the view |
| `src/OpenClaw.Tray.WinUI/Services/NodeService.cs` | `McpServerHost`, `CanvasWindowManager`, `MediaCapabilityHost`, `RecordingConsentService`, `NodeCapabilityRegistry` |
| `src/OpenClaw.Shared/OpenClawGatewayClient.cs` | `PendingRequestRegistry`, `ConnectEnvelopeBuilder`, `GatewayMessageRouter`, per-domain API facades |
| `src/OpenClaw.Shared/Models.cs` | per-domain model files + `*Mapper` classes |
Expand Down Expand Up @@ -134,8 +142,12 @@ leading and trailing pipe. Columns, in order:
| node-summary-text | authoritative | src/OpenClaw.Tray.WinUI/App.xaml.cs | node-summary clipboard text formatting | NodeSummaryText | App keeps the clipboard side effect (building the DataPackage and setting clipboard content) | copied node-summary text is projected only by NodeSummaryText.Build (online/offline state, display-name fallback, short id, detail text, newline join) | NodeSummaryTextTests.Build_MultipleNodes_OneLinePerNodeJoinedByNewline | behavioral | - |
| reactor-chat-timeline | authoritative | src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs | production chat message virtualization, row realization, and imperative scroll follow | ReactorChatTimeline through OpenClawReactorChatRoot and ReactorHostControl | OpenClawChatTimeline remains a legacy focused-test surface while its runtime route is migrated | the default chat route mounts one direct ReactorHostControl per XAML chat target; Reactor owns stable-key ItemsView and ItemContainer realization without a custom native list, collection reconciler, or scroll-layout mutation | review-only: user explicitly deferred new tests for this migration; required build and existing shared/tray suites still run | review-only | when Reactor timeline proof coverage replaces the legacy focused UI host coverage |
| functional-chat-default-mount | closed | src/OpenClaw.Tray.WinUI/Chat/FunctionalChatHostExtensions.cs | mounting the FunctionalUI chat tree as the default ChatPage or ChatWindow surface | ReactorChatHostExtensions and OpenClawReactorChatRoot | legacy FunctionalUI chat files may remain for focused compatibility coverage only | ChatPage and ChatWindow mount the Reactor root directly into their existing ChatHost Borders; no FunctionalUI component mounts or nests Reactor on the default path | review-only: user explicitly deferred new tests for this migration; required build and existing shared/tray suites still run | review-only | when legacy FunctionalUI chat surfaces are removed |
| settings-store | authoritative | src/OpenClaw.Tray.WinUI/Pages/SettingsPage.xaml.cs | hand-rolled save/echo suppression flags for two-way settings binding | ISettingsStore | PermissionsPage and other surfaces may read SettingsManager directly until migrated | a save originating from Update does not echo Changed to the caller and external saves are republished on the UI thread | SettingsStoreTests.Update_DoesNotEchoChangedToSelf | behavioral | when all settings surfaces read and write through ISettingsStore |
| settings-store | authoritative | settings and permission UI surfaces | direct SettingsManager mutation and blanket self-write suppression | ISettingsStore | non-permission legacy surfaces may read SettingsManager until migrated; direct saves publish origin null | every save publishes one versioned event; only the matching writer ignores its own origin while all other active consumers refresh | SettingsSharedStateContractTests.TwoActiveSettingsPageViewModels_IgnoreOnlyOwnWrites_InBothDirections | behavioral | when every settings surface reads and writes through ISettingsStore |
| settings-page-vm | authoritative | src/OpenClaw.Tray.WinUI/Pages/SettingsPage.xaml.cs | settings load, persist, echo-guard, and auto-save wiring | SettingsPageViewModel | code-behind keeps gateway-uninstall, gateway-info and uptime timer, saved-indicator visual, and app-info population | each settings control persists its field through the store preserving mutate-save-notify order and does not re-persist on external change | SettingsPageViewModelTests.ExternalChange_ReloadsWithoutRePersisting | behavioral | when the Settings page holds no settings persistence logic in code-behind |
| exec-approvals-store | authoritative | src/OpenClaw.Tray.WinUI/Pages/PermissionsPage.xaml.cs | direct exec-approvals.json snapshot, CAS persistence, file observation, and mutable policy cache | ExecApprovalsStore through IExecApprovalsPresentationStore | SystemCapability and NodeService consume the same App-owned concrete store for runtime enforcement | pure reads create nothing; CAS rejects stale hashes; one store-owned observer publishes each distinct external replacement once and retains the last valid presentation snapshot on typed failure | ExecApprovalsStoreTests.Changed_ExternalCorruptThenValid_RaisesFailureThenRecovery | behavioral | - |
| permissions-page-vm | authoritative | src/OpenClaw.Tray.WinUI/Pages/PermissionsPage.xaml.cs | permission settings state, exec-approvals mutations, node/MCP/voice status decisions, and lifecycle subscriptions | PermissionsPageViewModel plus PermissionsPageRuntimeSource | code-behind keeps exact WinUI row/card construction, localization application, colors, visibility, clipboard/token reads, privacy launch, and save-hint timer | activation is pure; field-scoped settings writes preserve save-then-notify; V2 mutations preserve unrelated fields through CAS retry; deactivate/dispose releases subscriptions | PermissionsPageViewModelTests.ExternalValidChange_UpdatesOnce_AndCorruptRetainsLastValidDisplay | behavioral | - |
| permissions-page-direct-owners-closed | closed | src/OpenClaw.Tray.WinUI/Pages/PermissionsPage.xaml.cs | direct SettingsManager, ConnectionManager, and ExecApprovalsStore ownership or subscriptions | PermissionsPageViewModel plus authoritative stores | WinUI-only rendering and platform actions listed in permissions-page-vm | the page applies semantic state only; the view model is WinUI/App/SettingsManager/file-IO free and never creates a parallel mutable domain cache | PermissionsPageContractTests.PermissionsPageViewModel_StaysWinUiAndAppFree | source-shape | when PermissionsPage is replaced by a different view technology |
| shared-mutable-domain-owner | authoritative | presentation pages and view models | backing-file or concrete-manager ownership, per-VM observers, and independent mutable copies of persisted domains | one observable service/store per shared mutable domain | immutable view state projected from authoritative snapshots | different active consumers converge through versioned origin-aware events or CAS snapshots without echo storms, stale whole-snapshot replay, or lost unrelated updates | SettingsSharedStateContractTests.PermissionsPageViewModel_ReceivesOneExternalUpdate_PerDistinctAppSurfaceOrigin | behavioral | - |
<!-- LEDGER:END -->

## Deferred test builders
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
using System;
using System.Threading;
using System.Threading.Tasks;

namespace OpenClaw.Shared.ExecApprovals;

public interface IExecApprovalsPresentationStore
{
event EventHandler<ExecApprovalsChangedEventArgs>? Changed;

Task<ExecApprovalsReadOnlySnapshotResult> GetSnapshotReadOnlyAsync(CancellationToken cancellationToken = default);

ExecApprovalsWriterOrigin CreateWriterOrigin();

Task<ExecApprovalsSnapshot?> ReplaceAsync(
string baseHash,
ExecApprovalsFile replacement,
ExecApprovalsWriterOrigin? origin,
Func<ExecApprovalsFile, ExecApprovalsFile, string?>? deltaValidator = null);
}

public sealed class ExecApprovalsWriterOrigin
{
internal ExecApprovalsWriterOrigin()
{
}
}

public enum ExecApprovalsChangeKind
{
SnapshotUpdated,
SnapshotRecovered,
SnapshotInvalid,
}

public enum ExecApprovalsSnapshotFailureKind
{
LegacyMigrationRequired,
UntrustedPath,
UnsupportedVersion,
MalformedJson,
ReadFailed,
}

public sealed record ExecApprovalsSnapshotFailure(
ExecApprovalsSnapshotFailureKind Kind,
string Hash,
int? Version,
string Message);

public sealed record ExecApprovalsReadOnlySnapshotResult(
ExecApprovalsSnapshot? Snapshot,
ExecApprovalsSnapshotFailure? Failure,
ExecApprovalsSnapshot? LastValidSnapshot)
{
public bool IsSuccess => Failure is null;
}

public sealed class ExecApprovalsChangedEventArgs : EventArgs
{
public ExecApprovalsChangedEventArgs(
long sequence,
ExecApprovalsChangeKind kind,
string hash,
int? version,
ExecApprovalsSnapshot? snapshot,
ExecApprovalsSnapshotFailure? failure,
ExecApprovalsSnapshot? lastValidSnapshot,
ExecApprovalsWriterOrigin? origin)
{
Sequence = sequence;
Kind = kind;
Hash = hash;
Version = version;
Snapshot = snapshot;
Failure = failure;
LastValidSnapshot = lastValidSnapshot;
Origin = origin;
}

public long Sequence { get; }

public ExecApprovalsChangeKind Kind { get; }

public string Hash { get; }

public int? Version { get; }

public ExecApprovalsSnapshot? Snapshot { get; }

public ExecApprovalsSnapshotFailure? Failure { get; }

public ExecApprovalsSnapshot? LastValidSnapshot { get; }

public ExecApprovalsWriterOrigin? Origin { get; }
}
Loading
Loading