@mohamedatia/fly-design-system 2.10.0 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -41,6 +41,29 @@ const FLYOS_LAUNCH_EVENT = 'flyos:launch';
41
41
  * Subsequent re-launches into the already-mounted remote arrive via the event.
42
42
  */
43
43
  const FlyosPendingLaunchesGlobalKey = '__FLYOS_PENDING_LAUNCHES__';
44
+ /**
45
+ * Per-window injection token carrying the active <see cref="WindowHelpHint"/>
46
+ * as a writable signal. The shell creates one writable signal per window
47
+ * (alongside <see cref="LAUNCH_CONTEXT"/>); apps inject it and `.set(...)` to
48
+ * publish or update their hint. Setting `null` clears the hint — the chrome
49
+ * falls back to the implicit `win.appId` deeplink.
50
+ *
51
+ * **Federation note:** same caveat as <see cref="LAUNCH_CONTEXT"/> — Native
52
+ * Federation can split the InjectionToken across host/remote bundles, so
53
+ * federated remotes cannot rely on DI to publish hints. Use
54
+ * <see cref="FLY_WINDOW_HELP_HINT_EVENT"/> instead.
55
+ */
56
+ const WINDOW_HELP_HINT = new InjectionToken('WINDOW_HELP_HINT');
57
+ /**
58
+ * Federation-safe window CustomEvent name for publishing a help hint from a
59
+ * federated remote that cannot see <see cref="WINDOW_HELP_HINT"/> via DI.
60
+ *
61
+ * Detail: <see cref="FlyWindowHelpHintEventDetail"/>. The shell listens at
62
+ * `window` scope and mirrors the payload into the matching per-window signal
63
+ * (keyed by `windowId` from <see cref="WINDOW_DATA"/>). Pairs with the
64
+ * <see cref="FLYOS_LAUNCH_EVENT"/> pattern.
65
+ */
66
+ const FLY_WINDOW_HELP_HINT_EVENT = 'flyos:window-help-hint';
44
67
 
45
68
  /**
46
69
  * Generic share / ACL panel models — hosts map domain DTOs to these shapes.
@@ -799,6 +822,54 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
799
822
  args: [{ providedIn: 'root' }]
800
823
  }] });
801
824
 
825
+ /**
826
+ * Publishes a {@link WindowHelpHint} for a window so the shell's titlebar Help
827
+ * button deeplinks the help-center reader to the article most relevant to where
828
+ * the user is. The **publisher twin** of the shell's `WindowHelpHintService`
829
+ * (the listener): it exists in the design system so that **any app — in-shell
830
+ * OS-Core app or federated remote — publishes hints the same way**, without
831
+ * hand-rolling the cross-bundle CustomEvent contract.
832
+ *
833
+ * **Why a `window` CustomEvent and not the `WINDOW_HELP_HINT` DI token?**
834
+ * Native Federation can split an `InjectionToken` instance across host and
835
+ * remote bundles, so a remote that injects `WINDOW_HELP_HINT` may receive a
836
+ * different token than the one the shell provides. The string-keyed
837
+ * {@link FLY_WINDOW_HELP_HINT_EVENT} crosses bundles reliably; the shell mirrors
838
+ * it into the matching per-window hint signal.
839
+ *
840
+ * **Why a per-window handle and not `bindWindow`/`setHint` state?**
841
+ * This service is `providedIn: 'root'` and the design system is a shared
842
+ * Native-Federation singleton, so a *single* instance is shared across the
843
+ * shell and every concurrently-open window (multiple in-shell apps, multiple
844
+ * remotes). A handle closes over its window id, so two windows never clobber a
845
+ * shared "current window" — each handle targets only its own titlebar.
846
+ */
847
+ class FlyWindowHelpService {
848
+ /**
849
+ * Returns a publisher bound to a single window. Call once per window (e.g.
850
+ * from the app root with `WINDOW_DATA.id`) and keep the handle. A
851
+ * null/undefined id (standalone, no shell) yields a handle whose `setHint`
852
+ * is a no-op.
853
+ */
854
+ forWindow(windowId) {
855
+ const id = windowId ?? null;
856
+ return {
857
+ setHint: (hint) => {
858
+ if (typeof window === 'undefined' || !id)
859
+ return;
860
+ const detail = { windowId: id, hint };
861
+ window.dispatchEvent(new CustomEvent(FLY_WINDOW_HELP_HINT_EVENT, { detail }));
862
+ },
863
+ };
864
+ }
865
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: FlyWindowHelpService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
866
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: FlyWindowHelpService, providedIn: 'root' });
867
+ }
868
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: FlyWindowHelpService, decorators: [{
869
+ type: Injectable,
870
+ args: [{ providedIn: 'root' }]
871
+ }] });
872
+
802
873
  /**
803
874
  * fly-remote-styles — Shell-layer CSS loader for Native Federation remotes.
804
875
  *
@@ -3359,7 +3430,7 @@ class AgentCommandRegistry {
3359
3430
  * for reactive filtering.
3360
3431
  */
3361
3432
  visible(liveAppIds) {
3362
- const liveSignal = isSignal(liveAppIds) ? liveAppIds : signal(liveAppIds).asReadonly();
3433
+ const liveSignal = isSignal$1(liveAppIds) ? liveAppIds : signal(liveAppIds).asReadonly();
3363
3434
  return computed(() => {
3364
3435
  const live = liveSignal();
3365
3436
  return this._commands().filter((cmd) => {
@@ -3432,6 +3503,111 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImpor
3432
3503
  args: [{ providedIn: 'root' }]
3433
3504
  }] });
3434
3505
  /** `isSignal` shim — narrows to either `Signal<T>` or a plain value. */
3506
+ function isSignal$1(v) {
3507
+ return typeof v === 'function';
3508
+ }
3509
+
3510
+ /**
3511
+ * Singleton registry of entity lookups offered by the `/lookup` typeahead.
3512
+ *
3513
+ * Mirrors {@link AgentCommandRegistry} exactly — same federation-singleton story
3514
+ * (`sharedMappings: ['@mohamedatia/fly-design-system']`), same id-collision
3515
+ * "latest wins" contract, same disposable-handle ergonomics. Federated remotes
3516
+ * register their lookupable entities at boot (Circles: scenario / trend / signal)
3517
+ * and dispose on window close, so the picker only ever offers entities whose app
3518
+ * is currently live.
3519
+ *
3520
+ * Storage is a signal store keyed on {@link LookupRegistration.entity}. Because
3521
+ * `entity` is the collision key, an app re-registering the same entity replaces
3522
+ * the prior descriptor; a stale handle's `dispose()` then no-ops.
3523
+ */
3524
+ class AgentLookupRegistry {
3525
+ _lookups = signal([], ...(ngDevMode ? [{ debugName: "_lookups" }] : /* istanbul ignore next */ []));
3526
+ /** All currently-registered lookups, in insertion order. */
3527
+ all = this._lookups.asReadonly();
3528
+ /**
3529
+ * Lookups whose scope is `'global'` OR whose `scope.appId` is in the live app
3530
+ * set. Recomputes when either the registry or `liveAppIds` changes — pass a
3531
+ * `Signal<ReadonlySet<string>>` from the host's app-registry for reactive
3532
+ * filtering, exactly like {@link AgentCommandRegistry.visible}.
3533
+ */
3534
+ visible(liveAppIds) {
3535
+ const liveSignal = isSignal(liveAppIds)
3536
+ ? liveAppIds
3537
+ : signal(liveAppIds).asReadonly();
3538
+ return computed(() => {
3539
+ const live = liveSignal();
3540
+ return this._lookups().filter((l) => {
3541
+ if (l.scope === 'global')
3542
+ return true;
3543
+ return live.has(l.scope.appId);
3544
+ });
3545
+ });
3546
+ }
3547
+ /**
3548
+ * Register one lookup. Returns a handle whose `dispose()` removes the row by
3549
+ * `entity`. A later re-registration of the same entity makes the original
3550
+ * handle's `dispose()` a no-op (the newer registration owns the row).
3551
+ */
3552
+ register(lookup) {
3553
+ const generation = ++this._generation;
3554
+ this._lookups.update((rows) => [
3555
+ ...rows.filter((r) => r.entity !== lookup.entity),
3556
+ lookup,
3557
+ ]);
3558
+ this._owners.set(lookup.entity, generation);
3559
+ return {
3560
+ dispose: () => {
3561
+ if (this._owners.get(lookup.entity) === generation) {
3562
+ this._owners.delete(lookup.entity);
3563
+ this._lookups.update((rows) => rows.filter((r) => r.entity !== lookup.entity));
3564
+ }
3565
+ },
3566
+ };
3567
+ }
3568
+ /**
3569
+ * Bulk register. Rolls back on a duplicate entity WITHIN the input batch
3570
+ * (throws before any row lands). Cross-batch duplicates against existing rows
3571
+ * follow the standard "latest wins" rule and do NOT trigger rollback.
3572
+ */
3573
+ registerAll(lookups) {
3574
+ const seen = new Set();
3575
+ for (const l of lookups) {
3576
+ if (seen.has(l.entity)) {
3577
+ throw new Error(`AgentLookupRegistry.registerAll: duplicate entity "${l.entity}" in batch`);
3578
+ }
3579
+ seen.add(l.entity);
3580
+ }
3581
+ const handles = lookups.map((l) => this.register(l));
3582
+ let disposed = false;
3583
+ return {
3584
+ dispose: () => {
3585
+ if (disposed)
3586
+ return;
3587
+ disposed = true;
3588
+ for (const h of handles)
3589
+ h.dispose();
3590
+ },
3591
+ };
3592
+ }
3593
+ /** Tear down by entity. Idempotent. */
3594
+ unregister(entity) {
3595
+ if (this._owners.delete(entity)) {
3596
+ this._lookups.update((rows) => rows.filter((r) => r.entity !== entity));
3597
+ }
3598
+ }
3599
+ /** Monotonic counter; identifies which registration call currently owns each entity. */
3600
+ _generation = 0;
3601
+ /** entity → generation. Lets a stale handle's `dispose()` no-op after replacement. */
3602
+ _owners = new Map();
3603
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AgentLookupRegistry, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
3604
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AgentLookupRegistry, providedIn: 'root' });
3605
+ }
3606
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AgentLookupRegistry, decorators: [{
3607
+ type: Injectable,
3608
+ args: [{ providedIn: 'root' }]
3609
+ }] });
3610
+ /** `isSignal` shim — narrows to either `Signal<T>` or a plain value. */
3435
3611
  function isSignal(v) {
3436
3612
  return typeof v === 'function';
3437
3613
  }
@@ -3539,6 +3715,21 @@ function compositeKey(kind, appId) {
3539
3715
  return `${kind}::${appId}`;
3540
3716
  }
3541
3717
 
3718
+ /**
3719
+ * Thrown synchronously by {@link AgentActionBus.dispatch} when a caller
3720
+ * supplies a dispatch mode this DS version doesn't implement yet. Catching
3721
+ * by class name lets a forward-compatible caller fall back to `'stage'`
3722
+ * without depending on instanceof across federation boundaries.
3723
+ */
3724
+ class AgentActionUnsupportedDispatchError extends Error {
3725
+ dispatch;
3726
+ constructor(dispatch) {
3727
+ super(`AgentActionBus: dispatch="${dispatch}" not supported in this DS version`);
3728
+ this.dispatch = dispatch;
3729
+ this.name = 'AgentActionUnsupportedDispatchError';
3730
+ }
3731
+ }
3732
+
3542
3733
  /**
3543
3734
  * Agent input contracts.
3544
3735
  *
@@ -3558,12 +3749,40 @@ function compositeKey(kind, appId) {
3558
3749
  */
3559
3750
  /** Frozen MIME used by `flyAgentDraggable` and the drop-zone reader. Never change without a DS major. */
3560
3751
  const AGENT_DRAG_MIME = 'application/x-fly-agent-payload+json';
3561
- /** Frozen payload envelope version. Bump only with a published DS major. */
3562
- const AGENT_PAYLOAD_VERSION = 1;
3752
+ /**
3753
+ * Frozen payload envelope version.
3754
+ *
3755
+ * v1 — minimal drag payload: kind / appId / version / payload / plainTextFallback /
3756
+ * suggestedCommandIds. Still used by the drag/drop surface.
3757
+ *
3758
+ * v2 — bus envelope ({@link AgentMessageEnvelope}). Adds optional `userMessage`,
3759
+ * `systemContext`, `attachments`, `mcpScope` so a dispatcher can give the agent
3760
+ * rich context without polluting the user-visible message bubble. v2 is a
3761
+ * superset of v1 — every v1 payload is a valid v2 payload — so the version
3762
+ * number ratchets forward without breaking existing callers.
3763
+ */
3764
+ const AGENT_PAYLOAD_VERSION = 2;
3765
+ /**
3766
+ * Versions accepted by the validator. v1 payloads (the drag/drop surface) remain valid;
3767
+ * v2 adds the optional bus-envelope fields. Renderers narrow on `version` when they need
3768
+ * to.
3769
+ */
3770
+ const SUPPORTED_AGENT_PAYLOAD_VERSIONS = Object.freeze([1, 2]);
3563
3771
  const DEFAULT_AGENT_PAYLOAD_LIMITS = Object.freeze({
3564
3772
  maxJsonBytes: 32 * 1024,
3565
3773
  maxPlainTextFallbackBytes: 8 * 1024,
3566
3774
  maxStringFieldBytes: 4 * 1024,
3775
+ maxUserMessageBytes: 280 * 4, // 280 chars × 4 bytes/char worst-case UTF-8
3776
+ maxSystemContextEntries: 32,
3777
+ maxSystemContextValueBytes: 4 * 1024,
3778
+ maxSystemContextJsonBytes: 8 * 1024,
3779
+ maxAttachments: 4,
3780
+ maxAttachmentJsonBytes: 16 * 1024,
3781
+ maxAttachmentTextBytes: 8 * 1024,
3782
+ maxAttachmentDataUrlBytes: 32 * 1024,
3783
+ maxMcpScopeApis: 5,
3784
+ maxMcpScopeApiBytes: 128,
3785
+ maxDeepLinkRouteBytes: 1024,
3567
3786
  });
3568
3787
 
3569
3788
  /**
@@ -3669,18 +3888,21 @@ function trimAgentString(input, maxBytes, ellipsis = '…') {
3669
3888
  * structured failure with the offending field path and byte counts.
3670
3889
  *
3671
3890
  * Order of checks (cheap → expensive):
3672
- * 1. `version` matches the frozen {@link AGENT_PAYLOAD_VERSION}.
3891
+ * 1. `version` is in {@link SUPPORTED_AGENT_PAYLOAD_VERSIONS}.
3673
3892
  * 2. `kind` is a non-empty string.
3674
3893
  * 3. `plainTextFallback` fits its cap.
3675
3894
  * 4. Every string in `payload` (recursively) fits the per-field cap.
3676
- * 5. The full `JSON.stringify(envelope)` fits the JSON cap.
3895
+ * 5. v2 sections `userMessage`, `systemContext`, `attachments`, `mcpScope`
3896
+ * each fits their independent caps. Skipped when absent so v1 payloads pass
3897
+ * unchanged.
3898
+ * 6. The full `JSON.stringify(envelope)` fits the JSON cap.
3677
3899
  *
3678
3900
  * The walker stops on the first oversize string field — the intent is to surface
3679
3901
  * actionable feedback, not enumerate every offender.
3680
3902
  */
3681
3903
  function validateAgentPayload(payload, limits) {
3682
3904
  const eff = { ...DEFAULT_AGENT_PAYLOAD_LIMITS, ...(limits ?? {}) };
3683
- if (payload.version !== AGENT_PAYLOAD_VERSION) {
3905
+ if (!SUPPORTED_AGENT_PAYLOAD_VERSIONS.includes(payload.version)) {
3684
3906
  return { ok: false, reason: 'invalid_version' };
3685
3907
  }
3686
3908
  if (typeof payload.kind !== 'string' || payload.kind.length === 0) {
@@ -3699,6 +3921,16 @@ function validateAgentPayload(payload, limits) {
3699
3921
  const fieldFailure = walkStringsForOverage(payload.payload, 'payload', eff.maxStringFieldBytes);
3700
3922
  if (fieldFailure)
3701
3923
  return fieldFailure;
3924
+ // v2 — per-section caps. Each helper is a no-op when its section is absent
3925
+ // so a v1 payload (no userMessage / systemContext / attachments / mcpScope /
3926
+ // deepLinkRoute) sails through unchanged.
3927
+ const v2Failure = checkUserMessage(payload, eff) ??
3928
+ checkSystemContext(payload, eff) ??
3929
+ checkAttachments(payload, eff) ??
3930
+ checkMcpScope(payload, eff) ??
3931
+ checkDeepLinkRoute(payload, eff);
3932
+ if (v2Failure)
3933
+ return v2Failure;
3702
3934
  const json = safeStringify(payload);
3703
3935
  const jsonBytes = utf8ByteLength(json);
3704
3936
  if (jsonBytes > eff.maxJsonBytes) {
@@ -3711,6 +3943,200 @@ function validateAgentPayload(payload, limits) {
3711
3943
  }
3712
3944
  return { ok: true };
3713
3945
  }
3946
+ // ─── v2 section validators ──────────────────────────────────────────────────
3947
+ function checkUserMessage(payload, eff) {
3948
+ if (payload.userMessage === undefined)
3949
+ return null;
3950
+ if (typeof payload.userMessage !== 'string')
3951
+ return null; // structural; ts catches at the call site
3952
+ const bytes = utf8ByteLength(payload.userMessage);
3953
+ if (bytes > eff.maxUserMessageBytes) {
3954
+ return {
3955
+ ok: false,
3956
+ reason: 'user_message_too_large',
3957
+ fieldPath: 'userMessage',
3958
+ actualBytes: bytes,
3959
+ limitBytes: eff.maxUserMessageBytes,
3960
+ };
3961
+ }
3962
+ return null;
3963
+ }
3964
+ function checkSystemContext(payload, eff) {
3965
+ const ctx = payload.systemContext;
3966
+ if (!ctx)
3967
+ return null;
3968
+ const keys = Object.keys(ctx);
3969
+ if (keys.length > eff.maxSystemContextEntries) {
3970
+ return {
3971
+ ok: false,
3972
+ reason: 'system_context_too_many_entries',
3973
+ fieldPath: 'systemContext',
3974
+ actualBytes: keys.length,
3975
+ limitBytes: eff.maxSystemContextEntries,
3976
+ };
3977
+ }
3978
+ for (const k of keys) {
3979
+ const v = ctx[k];
3980
+ if (typeof v !== 'string')
3981
+ continue;
3982
+ const bytes = utf8ByteLength(v);
3983
+ if (bytes > eff.maxSystemContextValueBytes) {
3984
+ return {
3985
+ ok: false,
3986
+ reason: 'system_context_value_too_large',
3987
+ fieldPath: `systemContext.${k}`,
3988
+ actualBytes: bytes,
3989
+ limitBytes: eff.maxSystemContextValueBytes,
3990
+ };
3991
+ }
3992
+ }
3993
+ const ctxJsonBytes = utf8ByteLength(safeStringify(ctx));
3994
+ if (ctxJsonBytes > eff.maxSystemContextJsonBytes) {
3995
+ return {
3996
+ ok: false,
3997
+ reason: 'system_context_json_too_large',
3998
+ fieldPath: 'systemContext',
3999
+ actualBytes: ctxJsonBytes,
4000
+ limitBytes: eff.maxSystemContextJsonBytes,
4001
+ };
4002
+ }
4003
+ return null;
4004
+ }
4005
+ function checkAttachments(payload, eff) {
4006
+ const atts = payload.attachments;
4007
+ if (!atts)
4008
+ return null;
4009
+ if (atts.length > eff.maxAttachments) {
4010
+ return {
4011
+ ok: false,
4012
+ reason: 'too_many_attachments',
4013
+ fieldPath: 'attachments',
4014
+ actualBytes: atts.length,
4015
+ limitBytes: eff.maxAttachments,
4016
+ };
4017
+ }
4018
+ for (let i = 0; i < atts.length; i++) {
4019
+ const a = atts[i];
4020
+ const path = `attachments[${i}]`;
4021
+ switch (a.kind) {
4022
+ case 'json': {
4023
+ const bytes = utf8ByteLength(safeStringify(a.json));
4024
+ if (bytes > eff.maxAttachmentJsonBytes) {
4025
+ return {
4026
+ ok: false,
4027
+ reason: 'attachment_json_too_large',
4028
+ fieldPath: `${path}.json`,
4029
+ actualBytes: bytes,
4030
+ limitBytes: eff.maxAttachmentJsonBytes,
4031
+ };
4032
+ }
4033
+ break;
4034
+ }
4035
+ case 'text': {
4036
+ const bytes = utf8ByteLength(a.text ?? '');
4037
+ if (bytes > eff.maxAttachmentTextBytes) {
4038
+ return {
4039
+ ok: false,
4040
+ reason: 'attachment_text_too_large',
4041
+ fieldPath: `${path}.text`,
4042
+ actualBytes: bytes,
4043
+ limitBytes: eff.maxAttachmentTextBytes,
4044
+ };
4045
+ }
4046
+ break;
4047
+ }
4048
+ case 'image':
4049
+ case 'file': {
4050
+ const bytes = utf8ByteLength(a.dataUrl ?? '');
4051
+ if (bytes > eff.maxAttachmentDataUrlBytes) {
4052
+ return {
4053
+ ok: false,
4054
+ reason: 'attachment_data_url_too_large',
4055
+ fieldPath: `${path}.dataUrl`,
4056
+ actualBytes: bytes,
4057
+ limitBytes: eff.maxAttachmentDataUrlBytes,
4058
+ };
4059
+ }
4060
+ break;
4061
+ }
4062
+ default:
4063
+ return {
4064
+ ok: false,
4065
+ reason: 'attachment_invalid_kind',
4066
+ fieldPath: `${path}.kind`,
4067
+ };
4068
+ }
4069
+ }
4070
+ return null;
4071
+ }
4072
+ /**
4073
+ * Validate `deepLinkRoute` — byte cap + minimal shape sanity check.
4074
+ *
4075
+ * The DS package is shell-agnostic, so full route grammar validation
4076
+ * (scheme prefixes, `..` segments, `//` runs) lives in the shell's
4077
+ * `DeepLinkService.isValidRoute` — the launcher is the authoritative
4078
+ * gate. Here we just enforce:
4079
+ * - byte cap so a malicious payload can't bloat the JSON
4080
+ * - must start with `/` so the value matches the launcher's contract
4081
+ * and a renderer can safely concatenate / display it.
4082
+ *
4083
+ * Anything more would force the DS to grow a second copy of the route
4084
+ * grammar; the launcher rejects malformed routes at click time so the
4085
+ * worst-case impact of a bad route landing here is a no-op click.
4086
+ */
4087
+ function checkDeepLinkRoute(payload, eff) {
4088
+ const route = payload.deepLinkRoute;
4089
+ if (route === undefined)
4090
+ return null;
4091
+ if (typeof route !== 'string')
4092
+ return null; // structural; ts catches at the call site
4093
+ if (!route.startsWith('/')) {
4094
+ return {
4095
+ ok: false,
4096
+ reason: 'deep_link_route_invalid_shape',
4097
+ fieldPath: 'deepLinkRoute',
4098
+ };
4099
+ }
4100
+ const bytes = utf8ByteLength(route);
4101
+ if (bytes > eff.maxDeepLinkRouteBytes) {
4102
+ return {
4103
+ ok: false,
4104
+ reason: 'deep_link_route_too_large',
4105
+ fieldPath: 'deepLinkRoute',
4106
+ actualBytes: bytes,
4107
+ limitBytes: eff.maxDeepLinkRouteBytes,
4108
+ };
4109
+ }
4110
+ return null;
4111
+ }
4112
+ function checkMcpScope(payload, eff) {
4113
+ const scope = payload.mcpScope;
4114
+ if (!scope)
4115
+ return null;
4116
+ const apis = scope.apis ?? [];
4117
+ if (apis.length > eff.maxMcpScopeApis) {
4118
+ return {
4119
+ ok: false,
4120
+ reason: 'mcp_scope_too_many_apis',
4121
+ fieldPath: 'mcpScope.apis',
4122
+ actualBytes: apis.length,
4123
+ limitBytes: eff.maxMcpScopeApis,
4124
+ };
4125
+ }
4126
+ for (let i = 0; i < apis.length; i++) {
4127
+ const bytes = utf8ByteLength(apis[i]);
4128
+ if (bytes > eff.maxMcpScopeApiBytes) {
4129
+ return {
4130
+ ok: false,
4131
+ reason: 'mcp_scope_api_too_large',
4132
+ fieldPath: `mcpScope.apis[${i}]`,
4133
+ actualBytes: bytes,
4134
+ limitBytes: eff.maxMcpScopeApiBytes,
4135
+ };
4136
+ }
4137
+ }
4138
+ return null;
4139
+ }
3714
4140
  /**
3715
4141
  * Returns a NEW payload with all string fields trimmed to fit the per-field cap, the
3716
4142
  * `plainTextFallback` trimmed to its cap, and the full envelope re-checked against the
@@ -3816,6 +4242,234 @@ function safeStringify(value) {
3816
4242
  });
3817
4243
  }
3818
4244
 
4245
+ /**
4246
+ * Imperative sibling to {@link AgentCommandRegistry} / {@link AgentDropRegistry}.
4247
+ *
4248
+ * Apps call {@link dispatch} to push a typed {@link AgentAction} onto the bus;
4249
+ * the agent panel subscribes once at construct and routes by verb. The bus
4250
+ * itself is a thin pass-through — it does NOT decide UI behaviour. The
4251
+ * subscriber (agent-panel) owns: showing the panel, staging the chip,
4252
+ * triggering the flight animation, and binding the command. This keeps the
4253
+ * DS free of host policy.
4254
+ *
4255
+ * Federation-safe: `providedIn: 'root'` + `sharedMappings: ['@mohamedatia/fly-design-system']`
4256
+ * give every federated remote the same singleton, so a remote's "Explain"
4257
+ * button reaches the host's panel without any cross-bundle wiring.
4258
+ *
4259
+ * Validation runs synchronously inside `dispatch` so a caller that sends an
4260
+ * oversize payload sees the throw at their site, not on the subscriber. The
4261
+ * subscriber therefore never has to defend against malformed envelopes.
4262
+ */
4263
+ class AgentActionBus {
4264
+ _actions$ = new Subject();
4265
+ /** Hot stream of actions in dispatch order. Subscribers receive only
4266
+ * actions dispatched AFTER they subscribe — late subscribers see nothing
4267
+ * retroactively. Use {@link lastAction} for the latest snapshot. */
4268
+ actions$ = this._actions$.asObservable();
4269
+ /** Most recent action — for DevTools, smoke tests, and late-subscriber
4270
+ * catch-up. Null until the first successful dispatch. */
4271
+ lastAction = signal(null, ...(ngDevMode ? [{ debugName: "lastAction" }] : /* istanbul ignore next */ []));
4272
+ /**
4273
+ * The action currently being processed by the subscriber, or null when
4274
+ * none. Set by {@link dispatch} immediately before emitting on
4275
+ * {@link actions$}; cleared by the subscriber via {@link settle} once
4276
+ * it finishes its handler (success or fail). Lets the dispatcher render
4277
+ * a busy state on the originating control — e.g. a card swapping its
4278
+ * sparkle icon for a spinner while the agent panel mints the optimistic
4279
+ * thread and starts the request. Identity check (`bus.inFlight() === act`)
4280
+ * is the panel-side contract; dispatchers usually project to a stable id
4281
+ * inside the payload (e.g. <c>reportId</c>) to scope busy-state visually.
4282
+ *
4283
+ * If multiple dispatches race, the latest wins — the prior in-flight
4284
+ * action is dropped on the floor here (the panel may still handle it,
4285
+ * but the dispatcher's busy indicator follows the newer action). Apps
4286
+ * that need stricter single-flight semantics should guard at the call
4287
+ * site (the agent-panel's <c>_pendingTempThreadId</c> already does so
4288
+ * for the explain verb).
4289
+ */
4290
+ inFlight = signal(null, ...(ngDevMode ? [{ debugName: "inFlight" }] : /* istanbul ignore next */ []));
4291
+ /**
4292
+ * Push an action onto the bus.
4293
+ *
4294
+ * Throws synchronously when:
4295
+ * - `dispatch === 'auto'` (not implemented in this DS version) — see
4296
+ * {@link AgentActionUnsupportedDispatchError}.
4297
+ * - the payload fails {@link validateAgentPayload} (oversize, invalid
4298
+ * version, invalid kind). The error message carries the field path
4299
+ * so the caller can fix the offending field.
4300
+ *
4301
+ * Subscribers see the action via {@link actions$} on the next tick of
4302
+ * the Subject; the {@link lastAction} signal updates synchronously
4303
+ * before the Subject emits so an effect reading both stays consistent.
4304
+ */
4305
+ dispatch(action) {
4306
+ if (action.dispatch === 'auto') {
4307
+ throw new AgentActionUnsupportedDispatchError(action.dispatch);
4308
+ }
4309
+ const v = validateAgentPayload(action.payload);
4310
+ if (!v.ok) {
4311
+ const where = v.fieldPath ? ` at ${v.fieldPath}` : '';
4312
+ throw new Error(`AgentActionBus.dispatch: invalid payload — ${v.reason}${where}`);
4313
+ }
4314
+ const a = action;
4315
+ this.lastAction.set(a);
4316
+ this.inFlight.set(a);
4317
+ this._actions$.next(a);
4318
+ }
4319
+ /**
4320
+ * Subscriber contract: call after the handler for {@link inFlight}
4321
+ * completes (success or fail). Only clears {@link inFlight} if it still
4322
+ * points at the passed action — a no-op when a later dispatch already
4323
+ * superseded it. Pass the same action reference the subscriber received
4324
+ * from {@link actions$}; identity is the gate.
4325
+ */
4326
+ settle(action) {
4327
+ if (this.inFlight() === action) {
4328
+ this.inFlight.set(null);
4329
+ }
4330
+ }
4331
+ /**
4332
+ * Semantic alias of {@link settle} for explicit user-driven cancellation —
4333
+ * e.g. a future "Stop" button in the agent input tray, or a dispatcher
4334
+ * teardown that wants to abandon its own in-flight action. Identical
4335
+ * runtime behaviour (identity check + clear), but the two-method surface
4336
+ * lets the UI distinguish "handler finished" from "user said no" in
4337
+ * telemetry / logs without sniffing a "reason" parameter.
4338
+ *
4339
+ * Pass the same action reference returned from {@link inFlight} or held
4340
+ * by the dispatcher; identity is the gate.
4341
+ */
4342
+ cancel(action) {
4343
+ if (this.inFlight() === action) {
4344
+ this.inFlight.set(null);
4345
+ }
4346
+ }
4347
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AgentActionBus, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4348
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AgentActionBus, providedIn: 'root' });
4349
+ }
4350
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AgentActionBus, decorators: [{
4351
+ type: Injectable,
4352
+ args: [{ providedIn: 'root' }]
4353
+ }] });
4354
+
4355
+ /**
4356
+ * FLIP-style entry animation for payloads landing in the agent panel.
4357
+ *
4358
+ * Pure DOM + Web Animations API — no Chart.js, no Angular animations module,
4359
+ * no CSS transitions racing layout. Honours `prefers-reduced-motion`: the
4360
+ * ghost is appended then removed without animating when the user asked for
4361
+ * less motion (so DOM side-effects stay consistent).
4362
+ *
4363
+ * Lifecycle:
4364
+ * 1. The agent panel calls {@link registerTarget} in `ngAfterViewInit`
4365
+ * with its header element.
4366
+ * 2. A source app dispatches an `AgentAction` carrying an `originRect`
4367
+ * from `getBoundingClientRect()` on the click target.
4368
+ * 3. The bus subscriber calls {@link flyInto} with that rect.
4369
+ * 4. The animator creates a fixed-position ghost at the origin, animates
4370
+ * transform + opacity toward the registered target's rect, then
4371
+ * removes itself on `onfinish` / `oncancel`.
4372
+ *
4373
+ * Uses `getBoundingClientRect()` (physical viewport coords) so the animation
4374
+ * is RTL-correct without inset-inline math — the rect already encodes the
4375
+ * physical position regardless of `dir`.
4376
+ *
4377
+ * The 900 ms duration and easing curve are deliberately hardcoded — making
4378
+ * them configurable surfaces an API the host can't usefully tune without
4379
+ * understanding motion design as a whole.
4380
+ */
4381
+ class AgentFlightAnimator {
4382
+ /** Hardcoded — see class doc. */
4383
+ static DURATION_MS = 900;
4384
+ static EASING = 'cubic-bezier(.2,.7,.2,1)';
4385
+ /** Floor the target/source scale ratio so a tiny target rect doesn't
4386
+ * collapse the ghost to invisibility before the animation finishes. */
4387
+ static MIN_SCALE = 0.05;
4388
+ targetEl = null;
4389
+ /** Called by the panel host to publish where flights should land. Pass
4390
+ * `null` on destroy so a re-mounted panel doesn't leave the animator
4391
+ * pointing at a detached node. */
4392
+ registerTarget(el) {
4393
+ this.targetEl = el;
4394
+ }
4395
+ /**
4396
+ * Animate a ghost element from {@link from} to the registered target's
4397
+ * rect. No-ops when:
4398
+ * - no target is registered (silent — panel may not be mounted yet)
4399
+ * - running outside a browser (SSR safety)
4400
+ * - the user has `prefers-reduced-motion: reduce` set (DOM is still
4401
+ * touched so callers see consistent side-effects, but no animation
4402
+ * runs)
4403
+ *
4404
+ * The ghost is appended to `document.body` (not the panel) so a parent
4405
+ * `overflow: hidden` on the panel can't clip the flight path.
4406
+ */
4407
+ flyInto(from, opts = {}) {
4408
+ if (!this.targetEl)
4409
+ return;
4410
+ if (typeof document === 'undefined')
4411
+ return;
4412
+ const reduced = typeof window !== 'undefined' && typeof window.matchMedia === 'function'
4413
+ ? window.matchMedia('(prefers-reduced-motion: reduce)').matches
4414
+ : false;
4415
+ const to = this.targetEl.getBoundingClientRect();
4416
+ const ghost = document.createElement('div');
4417
+ ghost.className = 'fly-agent-flight-ghost';
4418
+ // The ghost is purely decorative — it duplicates the source content
4419
+ // (chart title + snapshot) that the user just clicked and screen-reader
4420
+ // users have already heard via the sparkle button's aria-label. Re-
4421
+ // announcing the same content during the flight would be noisy at best
4422
+ // and confusing at worst. aria-hidden hides the subtree from assistive
4423
+ // tech while leaving it visible for sighted users.
4424
+ ghost.setAttribute('aria-hidden', 'true');
4425
+ // role="presentation" is belt-and-braces — even if a future renderer
4426
+ // walks the tree looking for semantic landmarks, the ghost has none.
4427
+ ghost.setAttribute('role', 'presentation');
4428
+ if (opts.previewHtml) {
4429
+ // Caller is responsible for safe HTML — the bus subscriber escapes
4430
+ // plainTextFallback before passing it here.
4431
+ ghost.innerHTML = opts.previewHtml;
4432
+ }
4433
+ Object.assign(ghost.style, {
4434
+ position: 'fixed',
4435
+ left: `${from.left}px`,
4436
+ top: `${from.top}px`,
4437
+ width: `${from.width}px`,
4438
+ height: `${from.height}px`,
4439
+ pointerEvents: 'none',
4440
+ zIndex: '99999',
4441
+ transformOrigin: 'top left',
4442
+ willChange: 'transform, opacity',
4443
+ });
4444
+ document.body.appendChild(ghost);
4445
+ if (reduced) {
4446
+ ghost.remove();
4447
+ return;
4448
+ }
4449
+ const dx = to.left - from.left;
4450
+ const dy = to.top - from.top;
4451
+ const sx = Math.max(to.width / from.width, AgentFlightAnimator.MIN_SCALE);
4452
+ const sy = Math.max(to.height / from.height, AgentFlightAnimator.MIN_SCALE);
4453
+ const anim = ghost.animate([
4454
+ { transform: 'translate(0,0) scale(1,1)', opacity: 1 },
4455
+ { transform: `translate(${dx}px,${dy}px) scale(${sx},${sy})`, opacity: 0.15 },
4456
+ ], {
4457
+ duration: AgentFlightAnimator.DURATION_MS,
4458
+ easing: AgentFlightAnimator.EASING,
4459
+ fill: 'forwards',
4460
+ });
4461
+ const cleanup = () => ghost.remove();
4462
+ anim.onfinish = cleanup;
4463
+ anim.oncancel = cleanup;
4464
+ }
4465
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AgentFlightAnimator, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
4466
+ static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AgentFlightAnimator, providedIn: 'root' });
4467
+ }
4468
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.5", ngImport: i0, type: AgentFlightAnimator, decorators: [{
4469
+ type: Injectable,
4470
+ args: [{ providedIn: 'root' }]
4471
+ }] });
4472
+
3819
4473
  /**
3820
4474
  * Strict whitelist for the ghost element id. Letter-led ASCII, allows word/hyphen/colon/dot,
3821
4475
  * capped at 128 chars. Anything else is rejected before reaching `getElementById` so a
@@ -4640,6 +5294,31 @@ function isRtlLocaleEntry(entry) {
4640
5294
  * centralising the canonicalization rule that strips `-service` from
4641
5295
  * JWT client_ids (`circles-service` → `circles`) so chips, banners,
4642
5296
  * and launchers stay consistent across the shell.
5297
+ * v2.6.0: Agent action bus + flight animator. New imperative sibling to
5298
+ * `AgentCommandRegistry` / `AgentDropRegistry`: `AgentActionBus` lets
5299
+ * any app push a typed `AgentAction { verb, payload, dispatch, originRect }`
5300
+ * to the agent panel without going through drag-drop. `AgentFlightAnimator`
5301
+ * plays a FLIP-style transform animation from the source DOM rect to the
5302
+ * registered panel-header rect (hardcoded 420 ms / cubic-bezier; honours
5303
+ * prefers-reduced-motion). Phase 1 supports `dispatch: 'stage'` only —
5304
+ * `'auto'` throws `AgentActionUnsupportedDispatchError` until
5305
+ * `AgentInputComponent.programmaticSubmit` ships. Re-uses
5306
+ * `AgentDragPayload<T>` as the wire envelope so drag and programmatic
5307
+ * transports never fork. New verbs: `explain`, `why-empty`,
5308
+ * `compose-query`, `compare`, `forecast`, `summarize`.
5309
+ * v2.10.0: Per-window help-deeplink contract. The shell renders a help-icon
5310
+ * button in every non-chromeless `<fly-window>` titlebar. By default
5311
+ * the deeplink uses `win.appId`; apps override or augment via the
5312
+ * new `WINDOW_HELP_HINT` InjectionToken (a `WritableSignal<WindowHelpHint | null>`)
5313
+ * provided per-window. `WindowHelpHint` is `{ appId?, topic? }` —
5314
+ * `topic` is a free-form search-query seed updated dynamically as the
5315
+ * user navigates within the app. New federation-safe channel
5316
+ * `FLY_WINDOW_HELP_HINT_EVENT` mirrors the DI surface for federated
5317
+ * remotes. New exports: `WindowHelpHint`, `WINDOW_HELP_HINT`,
5318
+ * `FLY_WINDOW_HELP_HINT_EVENT`, `FlyWindowHelpHintEventDetail`.
5319
+ * Backed by the help-center reader's new `params.topic` launch payload
5320
+ * and "no help for this app yet" empty-state. See
5321
+ * `skills/help-center.md` and `skills/desktop-shell-angular.md`.
4643
5322
  * v2.5.1: New `MessageBoxService.showAcknowledged()` entry point returning
4644
5323
  * `DialogResultWithAcknowledgement` for "Don't ask again"-style flows.
4645
5324
  * The `dontAskAgain` config (`MessageBoxDontAskAgainConfig`) lives ONLY on
@@ -4667,5 +5346,5 @@ const AUDIENCE_ERROR_CODES = {
4667
5346
  * Generated bundle index. Do not edit.
4668
5347
  */
4669
5348
 
4670
- export { AGENT_DRAG_MIME, AGENT_PAYLOAD_VERSION, APP_LOOKUP, AUDIENCE_ERROR_CODES, AUDIENCE_LIMITS, AUDIENCE_PRESETS, AUDIENCE_TERM_KINDS, AgentCommandRegistry, AgentDropRegistry, AgentPayloadOversizeError, AudienceBuilderComponent, AuthService, ContextMenuComponent, DEFAULT_AGENT_PAYLOAD_LIMITS, DEFAULT_FLY_THEME_MODE, DialogResult, FLYOS_LAUNCH_EVENT, FLY_LOCALE_CATALOG, FLY_REMOTE_BASE_PATH, FLY_REMOTE_ROUTES, FLY_THEME_MODE_IDS, FlyAgentDraggableDirective, FlyBlockUiComponent, FlyFileUploadComponent, FlyImageUploadComponent, FlyRemoteRouter, FlyRemoteRouterOutletComponent, FlySecureSrcDirective, FlyThemeService, FlyosPendingLaunchesGlobalKey, I18nService, LAUNCH_CONTEXT, MessageBoxButtons, MessageBoxComponent, MessageBoxIcon, MessageBoxService, MockAuthService, RTL_LOCALE_SET, SHARE_ORG_CHART_SYSTEM_KEY_APPS, SHARE_ORG_CHART_SYSTEM_KEY_DEFAULT, SHARE_PANEL_DEFAULT_FILE_LEVELS, SharePanelComponent, SourceAppResolver, StandaloneWindowManagerService, TranslatePipe, WINDOW_DATA, WindowManagerService, findLocaleByDialect, findLocaleByPrefix, isRtlLocale, isRtlLocaleEntry, loadRemoteStyles, matchFlyRoutePattern, normalizeFlyTheme, trimAgentPayload, trimAgentString, unloadRemoteStyles, utf8ByteLength, validateAgentPayload };
5349
+ export { AGENT_DRAG_MIME, AGENT_PAYLOAD_VERSION, APP_LOOKUP, AUDIENCE_ERROR_CODES, AUDIENCE_LIMITS, AUDIENCE_PRESETS, AUDIENCE_TERM_KINDS, AgentActionBus, AgentActionUnsupportedDispatchError, AgentCommandRegistry, AgentDropRegistry, AgentFlightAnimator, AgentLookupRegistry, AgentPayloadOversizeError, AudienceBuilderComponent, AuthService, ContextMenuComponent, DEFAULT_AGENT_PAYLOAD_LIMITS, DEFAULT_FLY_THEME_MODE, DialogResult, FLYOS_LAUNCH_EVENT, FLY_LOCALE_CATALOG, FLY_REMOTE_BASE_PATH, FLY_REMOTE_ROUTES, FLY_THEME_MODE_IDS, FLY_WINDOW_HELP_HINT_EVENT, FlyAgentDraggableDirective, FlyBlockUiComponent, FlyFileUploadComponent, FlyImageUploadComponent, FlyRemoteRouter, FlyRemoteRouterOutletComponent, FlySecureSrcDirective, FlyThemeService, FlyWindowHelpService, FlyosPendingLaunchesGlobalKey, I18nService, LAUNCH_CONTEXT, MessageBoxButtons, MessageBoxComponent, MessageBoxIcon, MessageBoxService, MockAuthService, RTL_LOCALE_SET, SHARE_ORG_CHART_SYSTEM_KEY_APPS, SHARE_ORG_CHART_SYSTEM_KEY_DEFAULT, SHARE_PANEL_DEFAULT_FILE_LEVELS, SUPPORTED_AGENT_PAYLOAD_VERSIONS, SharePanelComponent, SourceAppResolver, StandaloneWindowManagerService, TranslatePipe, WINDOW_DATA, WINDOW_HELP_HINT, WindowManagerService, findLocaleByDialect, findLocaleByPrefix, isRtlLocale, isRtlLocaleEntry, loadRemoteStyles, matchFlyRoutePattern, normalizeFlyTheme, trimAgentPayload, trimAgentString, unloadRemoteStyles, utf8ByteLength, validateAgentPayload };
4671
5350
  //# sourceMappingURL=mohamedatia-fly-design-system.mjs.map