@notis_ai/cli 0.2.0-beta.19.1 → 0.2.0-beta.196.1
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.
- package/README.md +822 -135
- package/bin/check-runtime.js +15 -0
- package/bin/notis.js +2 -0
- package/config/notis_app_boundary_rules.json +50 -0
- package/config/notis_app_design_rules.json +135 -0
- package/config/portal-route-roots.json +48 -0
- package/dist/agent-hooks/notis-agent-hook.mjs +45587 -0
- package/dist/base-skills/notis-apps/SKILL.md +93 -0
- package/dist/base-skills/notis-apps/references/architecture.md +114 -0
- package/dist/base-skills/notis-apps/references/context.md +81 -0
- package/dist/base-skills/notis-apps/references/design.md +97 -0
- package/dist/base-skills/notis-apps/references/memory.md +58 -0
- package/dist/base-skills/notis-apps/references/reading.md +67 -0
- package/dist/base-skills/notis-apps/references/release.md +107 -0
- package/dist/base-skills/notis-apps/references/sdk.md +116 -0
- package/dist/base-skills/notis-apps/references/troubleshooting.md +24 -0
- package/dist/base-skills/notis-apps/references/views.md +142 -0
- package/dist/base-skills/notis-cli/SKILL.md +154 -0
- package/dist/base-skills/notis-cli/references/app-delivery.md +19 -0
- package/dist/base-skills/notis-cli/references/intelligence.md +94 -0
- package/dist/base-skills/notis-cli/references/native-databases.md +21 -0
- package/dist/base-skills/notis-cli/references/tool-examples.md +56 -0
- package/dist/base-skills/notis-cli/references/troubleshooting.md +39 -0
- package/dist/base-skills/notis-query/SKILL.md +41 -0
- package/dist/base-skills/notis-query/references/database-discovery.md +60 -0
- package/dist/base-skills/notis-query/references/documents.md +63 -0
- package/dist/base-skills/notis-query/references/query.md +76 -0
- package/{template/packages/notis-sdk → dist/sdk}/package.json +14 -4
- package/dist/sdk/src/agentContext.ts +36 -0
- package/dist/sdk/src/bundleModule.ts +79 -0
- package/dist/sdk/src/components/Dialog.tsx +46 -0
- package/dist/sdk/src/components/DocumentEditor.tsx +106 -0
- package/dist/sdk/src/components/DocumentPage.tsx +141 -0
- package/dist/sdk/src/components/Markdown.tsx +60 -0
- package/dist/sdk/src/components/MarkdownEditor.tsx +121 -0
- package/dist/sdk/src/components/MultiSelectActionBar.tsx +285 -0
- package/dist/sdk/src/components/MultiSelectCheckbox.tsx +97 -0
- package/dist/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/dist/sdk/src/components/NotisCommentBoundary.tsx +172 -0
- package/dist/sdk/src/components/NotisSelectionBoundary.tsx +59 -0
- package/dist/sdk/src/components/RecordComponents.tsx +33 -0
- package/dist/sdk/src/components/RenderChartData.tsx +19 -0
- package/dist/sdk/src/components/ShortcutHints.tsx +56 -0
- package/dist/sdk/src/components/Skeleton.tsx +24 -0
- package/dist/sdk/src/config.ts +239 -0
- package/dist/sdk/src/documents.ts +258 -0
- package/dist/sdk/src/hooks/useAction.ts +28 -0
- package/dist/sdk/src/hooks/useActiveResource.ts +19 -0
- package/dist/sdk/src/hooks/useAgentContext.ts +23 -0
- package/dist/sdk/src/hooks/useCloudComputer.ts +95 -0
- package/dist/sdk/src/hooks/useCloudComputerShell.ts +114 -0
- package/dist/sdk/src/hooks/useCollectionInteractions.ts +838 -0
- package/dist/sdk/src/hooks/useDatabaseSchema.ts +49 -0
- package/dist/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
- package/dist/sdk/src/hooks/useDocument.ts +43 -0
- package/dist/sdk/src/hooks/useDocuments.ts +84 -0
- package/dist/sdk/src/hooks/useHandover.ts +78 -0
- package/dist/sdk/src/hooks/useLongPressSelection.ts +79 -0
- package/dist/sdk/src/hooks/useMultiSelect.ts +95 -0
- package/dist/sdk/src/hooks/useNativeDocumentBody.ts +50 -0
- package/dist/sdk/src/hooks/useNativeDocuments.ts +83 -0
- package/dist/sdk/src/hooks/useNotis.ts +42 -0
- package/dist/sdk/src/hooks/useNotisNavigation.ts +67 -0
- package/dist/sdk/src/hooks/useQuery.ts +74 -0
- package/dist/sdk/src/hooks/useTool.ts +65 -0
- package/dist/sdk/src/hooks/useToolQuery.ts +12 -0
- package/dist/sdk/src/hooks/useTopBarSearch.ts +81 -0
- package/dist/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/dist/sdk/src/hooks/useViewParams.ts +121 -0
- package/dist/sdk/src/hooks/useViewerReads.ts +129 -0
- package/dist/sdk/src/index.ts +200 -0
- package/dist/sdk/src/interactions/actions.ts +59 -0
- package/dist/sdk/src/interactions/shortcuts.tsx +740 -0
- package/dist/sdk/src/interactions/visibility.ts +13 -0
- package/dist/sdk/src/interactions.ts +45 -0
- package/dist/sdk/src/nativeData.ts +39 -0
- package/dist/sdk/src/presentation.tsx +82 -0
- package/dist/sdk/src/provider.tsx +49 -0
- package/dist/sdk/src/queryCache.ts +172 -0
- package/dist/sdk/src/queryErrors.ts +51 -0
- package/dist/sdk/src/runtime.ts +603 -0
- package/dist/sdk/src/space.ts +425 -0
- package/dist/sdk/src/styles.css +290 -0
- package/dist/sdk/src/tailwind.ts +66 -0
- package/dist/sdk/src/vite.ts +83 -0
- package/dist/skill-sync/index.js +2143 -0
- package/dist/skill-sync/index.js.map +7 -0
- package/dist/skill-sync-worker.mjs +3487 -0
- package/dist/space-harness/host.js +66 -0
- package/dist/view-renderer/dist/host.css +1 -0
- package/dist/view-renderer/dist/host.js +1042 -0
- package/dist/view-renderer/dist/theme.css +73 -0
- package/dist/view-renderer/src/dom.js +180 -0
- package/dist/view-renderer/src/errors.js +8 -0
- package/dist/view-renderer/src/host.tsx +173 -0
- package/dist/view-renderer/src/index.js +4 -0
- package/dist/view-renderer/src/pool.js +76 -0
- package/dist/view-renderer/src/remote-broker.js +59 -0
- package/dist/view-renderer/src/renderer.js +201 -0
- package/package.json +25 -8
- package/skills/notis-apps/cli.md +406 -0
- package/skills/notis-cli/AGENT_INSTRUCTIONS.md +39 -0
- package/skills/notis-onboarding/BRIEF.md +133 -0
- package/skills/notis-query/cli.md +39 -0
- package/src/agent-hook-entry.js +5 -0
- package/src/cli.js +294 -25
- package/src/command-specs/agents.js +392 -0
- package/src/command-specs/apps.js +1476 -203
- package/src/command-specs/auth.js +114 -137
- package/src/command-specs/diagnostics.js +730 -0
- package/src/command-specs/files.js +29 -0
- package/src/command-specs/handover.js +374 -0
- package/src/command-specs/helpers.js +86 -82
- package/src/command-specs/index.js +31 -6
- package/src/command-specs/meta.js +150 -18
- package/src/command-specs/onboarding.js +290 -0
- package/src/command-specs/profile.js +358 -0
- package/src/command-specs/skills.js +304 -0
- package/src/command-specs/smoke.js +386 -0
- package/src/command-specs/spaces.js +572 -0
- package/src/command-specs/tools.js +455 -139
- package/src/command-specs/views.js +69 -0
- package/src/runtime/agent-browser.js +700 -0
- package/src/runtime/agent-memory-state.js +126 -0
- package/src/runtime/agent-setup.js +383 -0
- package/src/runtime/app-boundary-validator.js +407 -0
- package/src/runtime/app-changelog.js +79 -0
- package/src/runtime/app-data-literals.js +82 -0
- package/src/runtime/app-platform.js +2752 -237
- package/src/runtime/app-registry-scaffolds.js +367 -0
- package/src/runtime/app-test-server.js +293 -0
- package/src/runtime/assets/store-screenshot-dark.png +0 -0
- package/src/runtime/auth-recovery.js +110 -0
- package/src/runtime/base-skills.d.ts +20 -0
- package/src/runtime/base-skills.js +167 -0
- package/src/runtime/channel.js +133 -0
- package/src/runtime/confined-space-build.js +74 -0
- package/src/runtime/confined-space-verify.js +49 -0
- package/src/runtime/delegated-context.js +68 -0
- package/src/runtime/errors.js +1 -0
- package/src/runtime/git.js +233 -0
- package/src/runtime/login-listener.js +15 -0
- package/src/runtime/native-record-files.js +47 -0
- package/src/runtime/oauth.js +2622 -0
- package/src/runtime/output.js +37 -5
- package/src/runtime/ports.js +31 -0
- package/src/runtime/profiles.js +906 -55
- package/src/runtime/retired-sdk-files.js +20 -0
- package/src/runtime/screenshot-framing.js +85 -0
- package/src/runtime/skill-sync/cloud-client.ts +121 -0
- package/src/runtime/skill-sync/index.ts +905 -0
- package/src/runtime/skill-sync/local-scanner.ts +1259 -0
- package/src/runtime/skill-sync/native-identity.ts +102 -0
- package/src/runtime/skill-sync/native-plan.ts +59 -0
- package/src/runtime/skill-sync/symlink-manager.ts +433 -0
- package/src/runtime/skill-sync/sync-plan.ts +54 -0
- package/src/runtime/skill-sync/types.ts +157 -0
- package/src/runtime/skill-sync/write-cloud-skill.ts +50 -0
- package/src/runtime/skill-sync-service.js +109 -0
- package/src/runtime/source-root.js +32 -0
- package/src/runtime/space-collection-scaffold.js +66 -0
- package/src/runtime/space-database-structures.js +229 -0
- package/src/runtime/space-fixture-broker.js +50 -0
- package/src/runtime/space-lock.js +337 -0
- package/src/runtime/space-platform.js +105 -0
- package/src/runtime/space-source.js +281 -0
- package/src/runtime/space-test-server.js +327 -0
- package/src/runtime/space-verification.js +89 -0
- package/src/runtime/space-verify-browser-worker.js +51 -0
- package/src/runtime/space-view-manifest.js +254 -0
- package/src/runtime/store-screenshot.js +146 -0
- package/src/runtime/sync-skills.d.ts +37 -0
- package/src/runtime/sync-skills.js +231 -0
- package/src/runtime/telemetry.js +92 -0
- package/src/runtime/transport.js +331 -45
- package/src/runtime/view-render.js +49 -0
- package/src/skill-sync-worker-entry.js +2 -0
- package/src/skill-sync-worker.js +50 -0
- package/src/space-harness-host.ts +103 -0
- package/template/.harness/index.html.tmpl +435 -0
- package/template/CHANGELOG.md +5 -0
- package/template/app/globals.css +29 -3
- package/template/app/layout.tsx +6 -3
- package/template/app/page.tsx +49 -42
- package/template/components/page-heading.tsx +23 -0
- package/template/components/ui/badge.tsx +7 -4
- package/template/components/ui/button.tsx +1 -1
- package/template/components/ui/card.tsx +24 -11
- package/template/components/ui/native-select.tsx +24 -0
- package/template/notis.config.ts +24 -6
- package/template/package-lock.json +3642 -0
- package/template/package.json +19 -16
- package/template/postcss.config.mjs +1 -1
- package/template/tailwind.config.ts +1 -6
- package/template/tsconfig.json +1 -0
- package/src/command-specs/db.js +0 -163
- package/src/runtime/app-preview-server.js +0 -312
- package/template/packages/notis-sdk/src/config.ts +0 -48
- package/template/packages/notis-sdk/src/helpers.ts +0 -131
- package/template/packages/notis-sdk/src/hooks/useAppState.ts +0 -50
- package/template/packages/notis-sdk/src/hooks/useCollectionItem.ts +0 -58
- package/template/packages/notis-sdk/src/hooks/useDatabase.ts +0 -87
- package/template/packages/notis-sdk/src/hooks/useDocument.ts +0 -61
- package/template/packages/notis-sdk/src/hooks/useNotis.ts +0 -31
- package/template/packages/notis-sdk/src/hooks/useNotisNavigation.ts +0 -49
- package/template/packages/notis-sdk/src/hooks/useTool.ts +0 -49
- package/template/packages/notis-sdk/src/hooks/useUpsertDocument.ts +0 -57
- package/template/packages/notis-sdk/src/index.ts +0 -47
- package/template/packages/notis-sdk/src/provider.tsx +0 -44
- package/template/packages/notis-sdk/src/runtime.ts +0 -159
- package/template/packages/notis-sdk/src/styles.css +0 -123
- package/template/packages/notis-sdk/src/vite.ts +0 -54
- package/template/packages/notis-sdk/tsconfig.json +0 -15
- /package/{template/packages/notis-sdk → dist/sdk}/src/hooks/useBackend.ts +0 -0
- /package/{template/packages/notis-sdk → dist/sdk}/src/hooks/useTools.ts +0 -0
- /package/{template/packages/notis-sdk → dist/sdk}/src/ui.ts +0 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Visibility across light and Shadow DOM, without relying on layout (virtual rows may have no rect). */
|
|
2
|
+
export function isInteractionElementVisible(element: HTMLElement | null): boolean {
|
|
3
|
+
if (!element?.isConnected) return false;
|
|
4
|
+
let current: Element | null = element;
|
|
5
|
+
while (current) {
|
|
6
|
+
if (current.hasAttribute('hidden') || current.hasAttribute('inert') || current.getAttribute('aria-hidden') === 'true') return false;
|
|
7
|
+
const style = current.ownerDocument.defaultView?.getComputedStyle(current);
|
|
8
|
+
if (style?.display === 'none' || style?.visibility === 'hidden') return false;
|
|
9
|
+
const root: Node = current.getRootNode();
|
|
10
|
+
current = current.parentElement ?? ('host' in root ? (root as ShadowRoot).host : null);
|
|
11
|
+
}
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export {
|
|
2
|
+
ShortcutProvider,
|
|
3
|
+
SHORTCUT_SCOPE_PRIORITY,
|
|
4
|
+
isEditableShortcutEvent,
|
|
5
|
+
shortcutDisplay,
|
|
6
|
+
useShortcuts,
|
|
7
|
+
} from './interactions/shortcuts';
|
|
8
|
+
export type {
|
|
9
|
+
ShortcutDefinition,
|
|
10
|
+
ShortcutHelpEntry,
|
|
11
|
+
ShortcutScope,
|
|
12
|
+
UseShortcutsOptions,
|
|
13
|
+
} from './interactions/shortcuts';
|
|
14
|
+
export { collectionActionDefaults } from './interactions/actions';
|
|
15
|
+
export type {
|
|
16
|
+
CollectionAction,
|
|
17
|
+
CollectionActionContext,
|
|
18
|
+
CollectionActionIntent,
|
|
19
|
+
ResolvedCollectionAction,
|
|
20
|
+
} from './interactions/actions';
|
|
21
|
+
export { useCollectionInteractions, COLLECTION_ITEM_ATTRIBUTE } from './hooks/useCollectionInteractions';
|
|
22
|
+
export type {
|
|
23
|
+
CollectionInteractionController,
|
|
24
|
+
CollectionInteractionReason,
|
|
25
|
+
CollectionKeyboardShortcuts,
|
|
26
|
+
CollectionNavigationContext,
|
|
27
|
+
CollectionNavigationDirection,
|
|
28
|
+
CollectionSelectionChange,
|
|
29
|
+
SelectionMarqueeRect,
|
|
30
|
+
UseCollectionInteractionsOptions,
|
|
31
|
+
} from './hooks/useCollectionInteractions';
|
|
32
|
+
export { MultiSelectActionBar, BULK_ACTION_LAYOUT_EVENT } from './components/MultiSelectActionBar';
|
|
33
|
+
export type { BulkActionLayoutDetail } from './components/MultiSelectActionBar';
|
|
34
|
+
export { MultiSelectCheckbox, MultiSelectCheckbox as SelectionCheckbox } from './components/MultiSelectCheckbox';
|
|
35
|
+
export { MultiSelectDragOverlay, MultiSelectDragOverlay as SelectionMarquee } from './components/MultiSelectDragOverlay';
|
|
36
|
+
export { ShortcutHints } from './components/ShortcutHints';
|
|
37
|
+
export type { MultiSelectAction, MultiSelectActionBarProps } from './components/MultiSelectActionBar';
|
|
38
|
+
export type { MultiSelectCheckboxProps } from './components/MultiSelectCheckbox';
|
|
39
|
+
export type { MultiSelectDragOverlayProps } from './components/MultiSelectDragOverlay';
|
|
40
|
+
export type { MultiSelectCheckboxProps as SelectionCheckboxProps } from './components/MultiSelectCheckbox';
|
|
41
|
+
export type { MultiSelectDragOverlayProps as SelectionMarqueeProps } from './components/MultiSelectDragOverlay';
|
|
42
|
+
export type { ShortcutHint, ShortcutHintsProps } from './components/ShortcutHints';
|
|
43
|
+
|
|
44
|
+
export { useLongPressSelection } from './hooks/useLongPressSelection';
|
|
45
|
+
export { isInteractionElementVisible } from './interactions/visibility';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** The native row contract, shared by direct tools and scoped Space actions. */
|
|
2
|
+
export type NativeField = { property_id: string } | { property: string } | {
|
|
3
|
+
column: 'record_key' | 'id' | 'title' | 'created_at' | 'updated_at' | 'last_edited_time' | 'content_type' | 'file_type';
|
|
4
|
+
};
|
|
5
|
+
export type NativeFilter = { and: NativeFilter[] } | { or: NativeFilter[] } | {
|
|
6
|
+
field: NativeField;
|
|
7
|
+
op: 'equals' | 'not_equals' | 'in' | 'contains' | 'not_contains' | 'does_not_contain'
|
|
8
|
+
| 'starts_with' | 'ends_with' | 'is_empty' | 'is_not_empty' | 'greater_than' | 'less_than'
|
|
9
|
+
| 'greater_than_or_equal_to' | 'less_than_or_equal_to' | 'before' | 'after' | 'on_or_before' | 'on_or_after';
|
|
10
|
+
value?: unknown;
|
|
11
|
+
};
|
|
12
|
+
export interface NativeQuery {
|
|
13
|
+
include_content?: boolean;
|
|
14
|
+
include_archived?: boolean;
|
|
15
|
+
mode?: 'rows' | 'search' | 'count' | 'aggregate';
|
|
16
|
+
filter?: NativeFilter;
|
|
17
|
+
sorts?: Array<{ field: NativeField; direction: 'asc' | 'desc' }>;
|
|
18
|
+
search?: { text: string; fields: NativeField[] };
|
|
19
|
+
aggregate?: { op: 'sum' | 'avg' | 'min' | 'max'; field: NativeField };
|
|
20
|
+
page_size?: number;
|
|
21
|
+
offset?: number;
|
|
22
|
+
}
|
|
23
|
+
export interface NativeInsert {
|
|
24
|
+
columns?: { title?: string | null; icon?: string | null; cover?: string | null };
|
|
25
|
+
properties?: Record<string, unknown>;
|
|
26
|
+
body?: { blocks: Array<Record<string, unknown>> };
|
|
27
|
+
}
|
|
28
|
+
export interface NativeUpdate extends NativeInsert { record_key: string; expected_revision: number; }
|
|
29
|
+
export interface NativeDelete { record_key: string; expected_revision: number; }
|
|
30
|
+
export interface NativeMutationReceipt {
|
|
31
|
+
record_key: string;
|
|
32
|
+
id: string;
|
|
33
|
+
database_id: string;
|
|
34
|
+
revision: number;
|
|
35
|
+
schema_revision: number;
|
|
36
|
+
receipt_id: string;
|
|
37
|
+
operation: 'insert' | 'update' | 'delete';
|
|
38
|
+
replayed?: boolean;
|
|
39
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { useCallback, useLayoutEffect, type CSSProperties, type ErrorInfo, type ReactNode } from 'react';
|
|
4
|
+
import { createPortal } from 'react-dom';
|
|
5
|
+
import { NotisProvider } from './provider';
|
|
6
|
+
import type { NotisRuntime } from './runtime';
|
|
7
|
+
|
|
8
|
+
type BoundaryProps = {
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
resetKey: string;
|
|
11
|
+
fallback: (error: Error) => ReactNode;
|
|
12
|
+
onError: (error: Error, info: ErrorInfo) => void;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/** A navigation can recover a crashed view without remounting healthy drafts. */
|
|
16
|
+
export class PresentationErrorBoundary extends React.Component<BoundaryProps, { error: Error | null; resetKey: string }> {
|
|
17
|
+
constructor(props: BoundaryProps) { super(props); this.state = { error: null, resetKey: props.resetKey }; }
|
|
18
|
+
static getDerivedStateFromError(error: Error) { return { error }; }
|
|
19
|
+
static getDerivedStateFromProps(props: BoundaryProps, state: { resetKey: string }) {
|
|
20
|
+
return props.resetKey !== state.resetKey ? { error: null, resetKey: props.resetKey } : null;
|
|
21
|
+
}
|
|
22
|
+
componentDidCatch(error: Error, info: ErrorInfo) { this.props.onError(error, info); }
|
|
23
|
+
render() { return this.state.error ? this.props.fallback(this.state.error) : this.props.children; }
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function CommittedPresentation({ mount, onCommit, children }: { mount: HTMLElement; onCommit?: (hasContent: boolean) => void; children: ReactNode }) {
|
|
27
|
+
useLayoutEffect(() => {
|
|
28
|
+
if (!onCommit) return;
|
|
29
|
+
const publish = () => onCommit?.(Boolean(mount.querySelector('[data-notis-app-root]')?.childNodes.length));
|
|
30
|
+
publish();
|
|
31
|
+
// A source may render null while its first query resolves. Observe the
|
|
32
|
+
// committed source DOM, not only commits of the unchanged host component.
|
|
33
|
+
const Observer = mount.ownerDocument.defaultView?.MutationObserver;
|
|
34
|
+
const observer = Observer ? new Observer(publish) : null;
|
|
35
|
+
observer?.observe(mount, { childList: true, subtree: true, characterData: true });
|
|
36
|
+
return () => observer?.disconnect();
|
|
37
|
+
}, [mount, onCommit]);
|
|
38
|
+
return children;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Portal and offline verification share this exact React/Shadow presentation.
|
|
42
|
+
* Source is trusted code in the host JS realm; Shadow DOM only isolates CSS.
|
|
43
|
+
* Loading, credentials and action authority belong to the respective host.
|
|
44
|
+
*/
|
|
45
|
+
export function SdkPresentation({ mount, runtime, component: Component, shell: Shell, theme, resetKey, active = true,
|
|
46
|
+
fallback, onError, onCommit, boundsCss, onProtocolError }: {
|
|
47
|
+
mount: HTMLElement | null;
|
|
48
|
+
runtime: NotisRuntime | null;
|
|
49
|
+
component: React.ComponentType | null;
|
|
50
|
+
shell?: React.ComponentType<{ children: ReactNode }>;
|
|
51
|
+
theme: { mode: 'light' | 'dark'; style: CSSProperties };
|
|
52
|
+
resetKey: string;
|
|
53
|
+
fallback: BoundaryProps['fallback'];
|
|
54
|
+
onError: BoundaryProps['onError'];
|
|
55
|
+
onCommit?: (hasContent: boolean) => void;
|
|
56
|
+
boundsCss?: string;
|
|
57
|
+
active?: boolean;
|
|
58
|
+
onProtocolError?: (error: Error) => void;
|
|
59
|
+
}) {
|
|
60
|
+
const shortcutsAvailable = useCallback(() => {
|
|
61
|
+
if (!active || !mount?.isConnected) return false;
|
|
62
|
+
let element: HTMLElement | null = mount;
|
|
63
|
+
while (element) {
|
|
64
|
+
const style = element.ownerDocument.defaultView?.getComputedStyle(element);
|
|
65
|
+
if (element.hidden || element.hasAttribute('inert') || element.getAttribute('aria-hidden') === 'true'
|
|
66
|
+
|| style?.display === 'none' || style?.visibility === 'hidden') return false;
|
|
67
|
+
const root = element.getRootNode();
|
|
68
|
+
element = element.parentElement || ('host' in root ? (root as ShadowRoot).host as HTMLElement : null);
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
}, [active, mount]);
|
|
72
|
+
return <PresentationErrorBoundary resetKey={resetKey} fallback={fallback} onError={onError}>
|
|
73
|
+
{mount && runtime && Component ? createPortal(<CommittedPresentation mount={mount} onCommit={onCommit}>
|
|
74
|
+
{boundsCss ? <style data-notis-runtime-bounds>{boundsCss}</style> : null}
|
|
75
|
+
<NotisProvider runtime={runtime} onProtocolError={onProtocolError} shortcutsAvailable={shortcutsAvailable}>
|
|
76
|
+
<div data-notis-app-root data-notis-theme={theme.mode} style={theme.style}>
|
|
77
|
+
{Shell ? <Shell><Component /></Shell> : <Component />}
|
|
78
|
+
</div>
|
|
79
|
+
</NotisProvider>
|
|
80
|
+
</CommittedPresentation>, mount) : null}
|
|
81
|
+
</PresentationErrorBoundary>;
|
|
82
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { createContext, useContext, useMemo, type Context, type ReactNode } from 'react';
|
|
4
|
+
import type { NotisRuntime } from './runtime';
|
|
5
|
+
import { ShortcutProvider } from './interactions/shortcuts';
|
|
6
|
+
import { guardSpaceRuntime } from './space';
|
|
7
|
+
|
|
8
|
+
const NOTIS_CONTEXT_SYMBOL = Symbol.for('notis.sdk.runtime_context');
|
|
9
|
+
|
|
10
|
+
type NotisContextGlobal = typeof globalThis & {
|
|
11
|
+
[NOTIS_CONTEXT_SYMBOL]?: Context<NotisRuntime | null>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function getNotisContext(): Context<NotisRuntime | null> {
|
|
15
|
+
const scope = globalThis as NotisContextGlobal;
|
|
16
|
+
if (!scope[NOTIS_CONTEXT_SYMBOL]) {
|
|
17
|
+
scope[NOTIS_CONTEXT_SYMBOL] = createContext<NotisRuntime | null>(null);
|
|
18
|
+
}
|
|
19
|
+
return scope[NOTIS_CONTEXT_SYMBOL]!;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const NotisContext = getNotisContext();
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Provides the Notis runtime context to all child components. The portal owns
|
|
26
|
+
* the runtime and injects it into the rendered app tree.
|
|
27
|
+
*/
|
|
28
|
+
export function NotisProvider({ children, runtime, onProtocolError, shortcutsAvailable }: {
|
|
29
|
+
children: ReactNode; runtime: NotisRuntime | null; onProtocolError?: (error: Error) => void;
|
|
30
|
+
shortcutsAvailable?: () => boolean;
|
|
31
|
+
}) {
|
|
32
|
+
const scopedRuntime = useMemo(() => guardSpaceRuntime(runtime, onProtocolError), [runtime, onProtocolError]);
|
|
33
|
+
if (runtime === undefined) {
|
|
34
|
+
throw new Error('NotisProvider requires an explicit runtime prop.');
|
|
35
|
+
}
|
|
36
|
+
return (
|
|
37
|
+
<NotisContext.Provider value={scopedRuntime}>
|
|
38
|
+
<ShortcutProvider isAvailable={shortcutsAvailable}>{children}</ShortcutProvider>
|
|
39
|
+
</NotisContext.Provider>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Returns the raw NotisRuntime or null if not yet available.
|
|
45
|
+
* Prefer the typed hooks (useTool, useNotis, etc.) over this.
|
|
46
|
+
*/
|
|
47
|
+
export function useNotisRuntime(): NotisRuntime | null {
|
|
48
|
+
return useContext(NotisContext);
|
|
49
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { SdkQueryError } from './queryErrors';
|
|
2
|
+
|
|
3
|
+
/** In-memory read snapshots. The host owns the lifetime and authorization scope. */
|
|
4
|
+
export interface QuerySnapshot<T = unknown> {
|
|
5
|
+
data: T | undefined;
|
|
6
|
+
hasData: boolean;
|
|
7
|
+
isFetching: boolean;
|
|
8
|
+
error: Error | null;
|
|
9
|
+
updatedAt: number;
|
|
10
|
+
invalidation: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface QueryFetchOptions {
|
|
14
|
+
staleTimeMs?: number;
|
|
15
|
+
force?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface NotisQueryClient {
|
|
19
|
+
getSnapshot<T>(key: string): QuerySnapshot<T>;
|
|
20
|
+
subscribe(key: string, listener: () => void): () => void;
|
|
21
|
+
fetch<T>(key: string, read: () => Promise<T>, options?: QueryFetchOptions): Promise<T>;
|
|
22
|
+
prefetch<T>(key: string, read: () => Promise<T>, options?: QueryFetchOptions): Promise<T>;
|
|
23
|
+
invalidate(key?: string): void;
|
|
24
|
+
clear(): void;
|
|
25
|
+
/** Host-only: permanently retire a revoked security scope. */
|
|
26
|
+
dispose?(): void;
|
|
27
|
+
getRevision?(): number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const EMPTY: QuerySnapshot = Object.freeze({
|
|
31
|
+
data: undefined, hasData: false, isFetching: false, error: null, updatedAt: 0, invalidation: 0,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
/** Stable keys distinguish filters, pagination, and resource identities. */
|
|
35
|
+
export function queryKey(value: unknown): string {
|
|
36
|
+
if (value instanceof Date) return JSON.stringify(value.toISOString());
|
|
37
|
+
if (Array.isArray(value)) return `[${value.map(queryKey).join(',')}]`;
|
|
38
|
+
if (value && typeof value === 'object') {
|
|
39
|
+
const record = value as Record<string, unknown>;
|
|
40
|
+
return `{${Object.keys(record).filter((key) => record[key] !== undefined).sort()
|
|
41
|
+
.map((key) => `${JSON.stringify(key)}:${queryKey(record[key])}`).join(',')}}`;
|
|
42
|
+
}
|
|
43
|
+
return JSON.stringify(value) ?? 'null';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** One shared queue can bound descriptor and data speculation together. */
|
|
47
|
+
export function createPrefetchQueue(concurrency = 2) {
|
|
48
|
+
let active = 0;
|
|
49
|
+
const waiting: Array<() => void> = [];
|
|
50
|
+
function drain() {
|
|
51
|
+
while (active < concurrency && waiting.length) waiting.shift()!();
|
|
52
|
+
}
|
|
53
|
+
return <T>(read: () => Promise<T>): Promise<T> => new Promise<T>((resolve, reject) => {
|
|
54
|
+
waiting.push(() => {
|
|
55
|
+
active += 1;
|
|
56
|
+
Promise.resolve().then(read).then(resolve, reject).finally(() => { active -= 1; drain(); });
|
|
57
|
+
});
|
|
58
|
+
drain();
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type Entry = {
|
|
63
|
+
snapshot: QuerySnapshot;
|
|
64
|
+
listeners: Set<() => void>;
|
|
65
|
+
generation: number;
|
|
66
|
+
pending?: Promise<unknown>;
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export function createQueryClient(options: {
|
|
70
|
+
maxEntries?: number;
|
|
71
|
+
now?: () => number;
|
|
72
|
+
schedule?: ReturnType<typeof createPrefetchQueue>;
|
|
73
|
+
/** Bounds idempotent reads only; never retries the underlying callback. */
|
|
74
|
+
readTimeoutMs?: number;
|
|
75
|
+
} = {}): NotisQueryClient {
|
|
76
|
+
const entries = new Map<string, Entry>();
|
|
77
|
+
const now = options.now ?? Date.now;
|
|
78
|
+
const maxEntries = options.maxEntries ?? 100;
|
|
79
|
+
const schedule = options.schedule ?? createPrefetchQueue(2);
|
|
80
|
+
let epoch = 0;
|
|
81
|
+
let disposed = false;
|
|
82
|
+
let revision = 0;
|
|
83
|
+
|
|
84
|
+
function prune(keep?: string) {
|
|
85
|
+
for (const [oldKey, old] of entries) {
|
|
86
|
+
if (entries.size <= maxEntries) break;
|
|
87
|
+
if (oldKey !== keep && !old.listeners.size && !old.pending) entries.delete(oldKey);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function entry(key: string): Entry {
|
|
91
|
+
let value = entries.get(key);
|
|
92
|
+
if (!value) {
|
|
93
|
+
value = { snapshot: EMPTY, listeners: new Set(), generation: 0 };
|
|
94
|
+
entries.set(key, value);
|
|
95
|
+
}
|
|
96
|
+
// Only operations, not getSnapshot during render, update the LRU.
|
|
97
|
+
entries.delete(key);
|
|
98
|
+
entries.set(key, value);
|
|
99
|
+
prune(key);
|
|
100
|
+
return value;
|
|
101
|
+
}
|
|
102
|
+
function publish(value: Entry, snapshot: QuerySnapshot) {
|
|
103
|
+
value.snapshot = snapshot;
|
|
104
|
+
for (const listener of value.listeners) listener();
|
|
105
|
+
}
|
|
106
|
+
const client: NotisQueryClient = {
|
|
107
|
+
getSnapshot<T>(key: string) { return (entries.get(key)?.snapshot ?? EMPTY) as QuerySnapshot<T>; },
|
|
108
|
+
subscribe(key, listener) {
|
|
109
|
+
const value = entry(key);
|
|
110
|
+
value.listeners.add(listener);
|
|
111
|
+
return () => { value.listeners.delete(listener); prune(); };
|
|
112
|
+
},
|
|
113
|
+
fetch<T>(key: string, read: () => Promise<T>, fetchOptions: QueryFetchOptions = {}) {
|
|
114
|
+
if (disposed) return Promise.reject(new SdkQueryError('scopeUnavailable'));
|
|
115
|
+
const value = entry(key);
|
|
116
|
+
if (value.pending) return value.pending as Promise<T>;
|
|
117
|
+
if (!fetchOptions.force && value.snapshot.hasData && value.snapshot.updatedAt > 0
|
|
118
|
+
&& now() - value.snapshot.updatedAt < (fetchOptions.staleTimeMs ?? 30_000)) {
|
|
119
|
+
return Promise.resolve(value.snapshot.data as T);
|
|
120
|
+
}
|
|
121
|
+
const generation = ++value.generation;
|
|
122
|
+
const requestEpoch = epoch;
|
|
123
|
+
const canCommit = () => epoch === requestEpoch && value.generation === generation && entries.get(key) === value;
|
|
124
|
+
let timeout: ReturnType<typeof setTimeout>;
|
|
125
|
+
const deadline = new Promise<never>((_, reject) => {
|
|
126
|
+
timeout = setTimeout(() => reject(new SdkQueryError('timedOut')), options.readTimeoutMs ?? 30_000);
|
|
127
|
+
// Node-based verification must not stay alive for a retired browser read.
|
|
128
|
+
if (typeof timeout === 'object' && 'unref' in timeout) timeout.unref();
|
|
129
|
+
});
|
|
130
|
+
const request = Promise.race([Promise.resolve().then(read), deadline]).then((data) => {
|
|
131
|
+
if (canCommit()) publish(value, { ...value.snapshot, data, hasData: true, isFetching: false, error: null, updatedAt: now() });
|
|
132
|
+
return data;
|
|
133
|
+
}, (reason) => {
|
|
134
|
+
const error = reason instanceof Error ? reason : new Error(String(reason));
|
|
135
|
+
if (canCommit()) publish(value, { ...value.snapshot, isFetching: false, error });
|
|
136
|
+
throw error;
|
|
137
|
+
}).finally(() => { clearTimeout(timeout); if (value.pending === request) value.pending = undefined; prune(); });
|
|
138
|
+
value.pending = request;
|
|
139
|
+
publish(value, { ...value.snapshot, isFetching: true, error: null });
|
|
140
|
+
return request;
|
|
141
|
+
},
|
|
142
|
+
prefetch<T>(key: string, read: () => Promise<T>, fetchOptions?: QueryFetchOptions) {
|
|
143
|
+
const requestEpoch = epoch;
|
|
144
|
+
return schedule(() => {
|
|
145
|
+
if (epoch !== requestEpoch) throw new SdkQueryError('scopeCleared');
|
|
146
|
+
return client.fetch(key, read, fetchOptions);
|
|
147
|
+
});
|
|
148
|
+
},
|
|
149
|
+
getRevision: () => revision,
|
|
150
|
+
invalidate(key) {
|
|
151
|
+
revision += 1;
|
|
152
|
+
for (const [entryKey, value] of entries) {
|
|
153
|
+
if (key !== undefined && entryKey !== key) continue;
|
|
154
|
+
value.generation += 1;
|
|
155
|
+
value.pending = undefined;
|
|
156
|
+
publish(value, { ...value.snapshot, updatedAt: 0, isFetching: false, invalidation: value.snapshot.invalidation + 1 });
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
dispose() { disposed = true; client.clear(); },
|
|
160
|
+
clear() {
|
|
161
|
+
revision += 1;
|
|
162
|
+
epoch += 1;
|
|
163
|
+
for (const value of entries.values()) {
|
|
164
|
+
value.generation += 1;
|
|
165
|
+
value.pending = undefined;
|
|
166
|
+
publish(value, { ...EMPTY, invalidation: value.snapshot.invalidation + 1 });
|
|
167
|
+
}
|
|
168
|
+
for (const [key, value] of entries) if (!value.listeners.size) entries.delete(key);
|
|
169
|
+
},
|
|
170
|
+
};
|
|
171
|
+
return client;
|
|
172
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/** SDK-owned read failures. The host still owns messages for server/action refusals. */
|
|
2
|
+
const copy = {
|
|
3
|
+
en: {
|
|
4
|
+
bindingUnavailable: 'This Space cannot read this database yet.',
|
|
5
|
+
actionUnavailable: 'This action is unavailable in this Space.',
|
|
6
|
+
readUnavailable: 'These data cannot be read in this Space.',
|
|
7
|
+
schemaChanged: 'This database changed. Refresh the page and try again.',
|
|
8
|
+
responseIncomplete: 'This database could not be loaded. Refresh the page and try again.',
|
|
9
|
+
listUnavailable: 'This list is unavailable in this view.',
|
|
10
|
+
timedOut: 'This read took too long. Please retry.',
|
|
11
|
+
scopeUnavailable: 'App query scope is no longer available.',
|
|
12
|
+
scopeCleared: 'App query scope was cleared.',
|
|
13
|
+
},
|
|
14
|
+
fr: {
|
|
15
|
+
bindingUnavailable: 'Ce Space ne peut pas encore lire cette base de données.',
|
|
16
|
+
actionUnavailable: 'Cette action n’est pas disponible dans ce Space.',
|
|
17
|
+
readUnavailable: 'Ces données ne sont pas accessibles dans ce Space.',
|
|
18
|
+
schemaChanged: 'Cette base de données a changé. Actualise la page et réessaie.',
|
|
19
|
+
responseIncomplete: 'Cette base de données n’a pas pu être chargée. Actualise la page et réessaie.',
|
|
20
|
+
listUnavailable: 'Cette liste n’est pas disponible dans cette vue.',
|
|
21
|
+
timedOut: 'Cette lecture a pris trop de temps. Réessaie.',
|
|
22
|
+
scopeUnavailable: 'Les données de cette vue ne sont plus accessibles.',
|
|
23
|
+
scopeCleared: 'Les données de cette vue ont été réinitialisées.',
|
|
24
|
+
},
|
|
25
|
+
} as const;
|
|
26
|
+
|
|
27
|
+
export type QueryErrorKey = keyof typeof copy.en;
|
|
28
|
+
const PREFIX = 'notis_sdk_query:';
|
|
29
|
+
|
|
30
|
+
/** Code and diagnostic survive localisation; only SDK-owned messages are translated. */
|
|
31
|
+
export class SdkQueryError extends Error {
|
|
32
|
+
readonly code: string;
|
|
33
|
+
readonly diagnostic?: string;
|
|
34
|
+
constructor(key: QueryErrorKey, diagnostic?: string, locale?: string, cause?: Error) {
|
|
35
|
+
super(copy[locale === 'fr' ? 'fr' : 'en'][key], cause ? { cause } : undefined);
|
|
36
|
+
this.name = 'SdkQueryError';
|
|
37
|
+
this.code = PREFIX + key;
|
|
38
|
+
this.diagnostic = diagnostic;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Called at render time, so a cached failure changes language without replaying its read. */
|
|
43
|
+
export function localizeQueryError(error: Error | null, locale?: string): Error | null {
|
|
44
|
+
const failure = error as (Error & { code?: unknown; diagnostic?: unknown }) | null;
|
|
45
|
+
if (!failure || typeof failure.code !== 'string' || !failure.code.startsWith(PREFIX)) return error;
|
|
46
|
+
const key = failure.code.slice(PREFIX.length);
|
|
47
|
+
if (!Object.hasOwn(copy.en, key)) return error;
|
|
48
|
+
const message = copy[locale === 'fr' ? 'fr' : 'en'][key as QueryErrorKey];
|
|
49
|
+
if (failure.message === message) return error;
|
|
50
|
+
return new SdkQueryError(key as QueryErrorKey, typeof failure.diagnostic === 'string' ? failure.diagnostic : undefined, locale, failure);
|
|
51
|
+
}
|