@notis_ai/cli 0.2.0-beta.191.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.
Files changed (106) hide show
  1. package/README.md +479 -80
  2. package/config/portal-route-roots.json +48 -0
  3. package/dist/agent-hooks/notis-agent-hook.mjs +29072 -2784
  4. package/dist/base-skills/notis-apps/SKILL.md +88 -67
  5. package/dist/base-skills/notis-apps/references/architecture.md +114 -167
  6. package/dist/base-skills/notis-apps/references/design.md +26 -107
  7. package/dist/base-skills/notis-apps/references/memory.md +58 -0
  8. package/dist/base-skills/notis-apps/references/reading.md +52 -74
  9. package/dist/base-skills/notis-apps/references/release.md +104 -142
  10. package/dist/base-skills/notis-apps/references/sdk.md +113 -60
  11. package/dist/base-skills/notis-apps/references/troubleshooting.md +22 -21
  12. package/dist/base-skills/notis-apps/references/views.md +142 -0
  13. package/dist/base-skills/notis-cli/SKILL.md +4 -4
  14. package/dist/base-skills/notis-cli/references/app-delivery.md +12 -11
  15. package/dist/base-skills/notis-cli/references/native-databases.md +6 -6
  16. package/dist/base-skills/notis-cli/references/tool-examples.md +4 -4
  17. package/dist/base-skills/notis-query/SKILL.md +34 -60
  18. package/dist/base-skills/notis-query/references/database-discovery.md +59 -69
  19. package/dist/base-skills/notis-query/references/documents.md +63 -50
  20. package/dist/base-skills/notis-query/references/query.md +57 -524
  21. package/dist/sdk/src/bundleModule.ts +79 -0
  22. package/dist/sdk/src/components/DocumentEditor.tsx +6 -3
  23. package/dist/sdk/src/components/DocumentPage.tsx +141 -0
  24. package/dist/sdk/src/components/RecordComponents.tsx +33 -0
  25. package/dist/sdk/src/components/RenderChartData.tsx +19 -0
  26. package/dist/sdk/src/config.ts +4 -26
  27. package/dist/sdk/src/documents.ts +2 -0
  28. package/dist/sdk/src/hooks/useAction.ts +28 -0
  29. package/dist/sdk/src/hooks/useCloudComputer.ts +35 -4
  30. package/dist/sdk/src/hooks/useCloudComputerShell.ts +114 -0
  31. package/dist/sdk/src/hooks/useHandover.ts +5 -4
  32. package/dist/sdk/src/hooks/useNativeDocumentBody.ts +50 -0
  33. package/dist/sdk/src/hooks/useNativeDocuments.ts +83 -0
  34. package/dist/sdk/src/hooks/useNotis.ts +3 -0
  35. package/dist/sdk/src/hooks/useNotisNavigation.ts +26 -11
  36. package/dist/sdk/src/hooks/useQuery.ts +7 -4
  37. package/dist/sdk/src/hooks/useViewParams.ts +121 -0
  38. package/dist/sdk/src/hooks/useViewerReads.ts +129 -0
  39. package/dist/sdk/src/index.ts +36 -0
  40. package/dist/sdk/src/interactions/shortcuts.tsx +17 -3
  41. package/dist/sdk/src/nativeData.ts +39 -0
  42. package/dist/sdk/src/presentation.tsx +82 -0
  43. package/dist/sdk/src/provider.tsx +9 -4
  44. package/dist/sdk/src/queryCache.ts +5 -3
  45. package/dist/sdk/src/queryErrors.ts +51 -0
  46. package/dist/sdk/src/runtime.ts +146 -8
  47. package/dist/sdk/src/space.ts +425 -0
  48. package/dist/sdk/src/styles.css +96 -72
  49. package/dist/sdk/src/vite.ts +11 -1
  50. package/dist/skill-sync/index.js +458 -68
  51. package/dist/skill-sync/index.js.map +4 -4
  52. package/dist/skill-sync-worker.mjs +451 -77
  53. package/dist/space-harness/host.js +66 -0
  54. package/dist/view-renderer/dist/host.css +1 -0
  55. package/dist/view-renderer/dist/host.js +1042 -0
  56. package/dist/view-renderer/dist/theme.css +73 -0
  57. package/dist/view-renderer/src/dom.js +180 -0
  58. package/dist/view-renderer/src/errors.js +8 -0
  59. package/dist/view-renderer/src/host.tsx +173 -0
  60. package/dist/view-renderer/src/index.js +4 -0
  61. package/dist/view-renderer/src/pool.js +76 -0
  62. package/dist/view-renderer/src/remote-broker.js +59 -0
  63. package/dist/view-renderer/src/renderer.js +201 -0
  64. package/package.json +11 -5
  65. package/skills/notis-apps/cli.md +270 -163
  66. package/skills/notis-cli/AGENT_INSTRUCTIONS.md +2 -2
  67. package/skills/notis-onboarding/BRIEF.md +7 -3
  68. package/src/command-specs/apps.js +1 -8
  69. package/src/command-specs/files.js +29 -0
  70. package/src/command-specs/helpers.js +1 -0
  71. package/src/command-specs/index.js +10 -4
  72. package/src/command-specs/skills.js +229 -0
  73. package/src/command-specs/spaces.js +572 -0
  74. package/src/command-specs/views.js +69 -0
  75. package/src/runtime/agent-browser.js +30 -7
  76. package/src/runtime/app-boundary-validator.js +7 -4
  77. package/src/runtime/app-data-literals.js +82 -0
  78. package/src/runtime/app-platform.js +321 -253
  79. package/src/runtime/confined-space-build.js +74 -0
  80. package/src/runtime/confined-space-verify.js +49 -0
  81. package/src/runtime/native-record-files.js +47 -0
  82. package/src/runtime/retired-sdk-files.js +20 -0
  83. package/src/runtime/skill-sync/cloud-client.ts +26 -4
  84. package/src/runtime/skill-sync/index.ts +154 -35
  85. package/src/runtime/skill-sync/local-scanner.ts +125 -17
  86. package/src/runtime/skill-sync/native-identity.ts +102 -0
  87. package/src/runtime/skill-sync/native-plan.ts +59 -0
  88. package/src/runtime/skill-sync/sync-plan.ts +16 -9
  89. package/src/runtime/skill-sync/types.ts +32 -6
  90. package/src/runtime/source-root.js +32 -0
  91. package/src/runtime/space-collection-scaffold.js +66 -0
  92. package/src/runtime/space-database-structures.js +229 -0
  93. package/src/runtime/space-fixture-broker.js +50 -0
  94. package/src/runtime/space-lock.js +337 -0
  95. package/src/runtime/space-platform.js +105 -0
  96. package/src/runtime/space-source.js +281 -0
  97. package/src/runtime/space-test-server.js +327 -0
  98. package/src/runtime/space-verification.js +89 -0
  99. package/src/runtime/space-verify-browser-worker.js +51 -0
  100. package/src/runtime/space-view-manifest.js +254 -0
  101. package/src/runtime/sync-skills.js +1 -1
  102. package/src/runtime/transport.js +8 -1
  103. package/src/runtime/view-render.js +49 -0
  104. package/src/space-harness-host.ts +103 -0
  105. package/template/app/globals.css +1 -0
  106. package/src/command-specs/reports.js +0 -97
@@ -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
+ }
@@ -1,8 +1,9 @@
1
1
  'use client';
2
2
 
3
- import React, { createContext, useContext, type Context, type ReactNode } from 'react';
3
+ import React, { createContext, useContext, useMemo, type Context, type ReactNode } from 'react';
4
4
  import type { NotisRuntime } from './runtime';
5
5
  import { ShortcutProvider } from './interactions/shortcuts';
6
+ import { guardSpaceRuntime } from './space';
6
7
 
7
8
  const NOTIS_CONTEXT_SYMBOL = Symbol.for('notis.sdk.runtime_context');
8
9
 
@@ -24,13 +25,17 @@ const NotisContext = getNotisContext();
24
25
  * Provides the Notis runtime context to all child components. The portal owns
25
26
  * the runtime and injects it into the rendered app tree.
26
27
  */
27
- export function NotisProvider({ children, runtime }: { children: ReactNode; runtime: NotisRuntime | null }) {
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]);
28
33
  if (runtime === undefined) {
29
34
  throw new Error('NotisProvider requires an explicit runtime prop.');
30
35
  }
31
36
  return (
32
- <NotisContext.Provider value={runtime}>
33
- <ShortcutProvider>{children}</ShortcutProvider>
37
+ <NotisContext.Provider value={scopedRuntime}>
38
+ <ShortcutProvider isAvailable={shortcutsAvailable}>{children}</ShortcutProvider>
34
39
  </NotisContext.Provider>
35
40
  );
36
41
  }
@@ -1,3 +1,5 @@
1
+ import { SdkQueryError } from './queryErrors';
2
+
1
3
  /** In-memory read snapshots. The host owns the lifetime and authorization scope. */
2
4
  export interface QuerySnapshot<T = unknown> {
3
5
  data: T | undefined;
@@ -109,7 +111,7 @@ export function createQueryClient(options: {
109
111
  return () => { value.listeners.delete(listener); prune(); };
110
112
  },
111
113
  fetch<T>(key: string, read: () => Promise<T>, fetchOptions: QueryFetchOptions = {}) {
112
- if (disposed) return Promise.reject(new Error('App query scope is no longer available.'));
114
+ if (disposed) return Promise.reject(new SdkQueryError('scopeUnavailable'));
113
115
  const value = entry(key);
114
116
  if (value.pending) return value.pending as Promise<T>;
115
117
  if (!fetchOptions.force && value.snapshot.hasData && value.snapshot.updatedAt > 0
@@ -121,7 +123,7 @@ export function createQueryClient(options: {
121
123
  const canCommit = () => epoch === requestEpoch && value.generation === generation && entries.get(key) === value;
122
124
  let timeout: ReturnType<typeof setTimeout>;
123
125
  const deadline = new Promise<never>((_, reject) => {
124
- timeout = setTimeout(() => reject(new Error('This read took too long. Please retry.')), options.readTimeoutMs ?? 30_000);
126
+ timeout = setTimeout(() => reject(new SdkQueryError('timedOut')), options.readTimeoutMs ?? 30_000);
125
127
  // Node-based verification must not stay alive for a retired browser read.
126
128
  if (typeof timeout === 'object' && 'unref' in timeout) timeout.unref();
127
129
  });
@@ -140,7 +142,7 @@ export function createQueryClient(options: {
140
142
  prefetch<T>(key: string, read: () => Promise<T>, fetchOptions?: QueryFetchOptions) {
141
143
  const requestEpoch = epoch;
142
144
  return schedule(() => {
143
- if (epoch !== requestEpoch) throw new Error('App query scope was cleared.');
145
+ if (epoch !== requestEpoch) throw new SdkQueryError('scopeCleared');
144
146
  return client.fetch(key, read, fetchOptions);
145
147
  });
146
148
  },
@@ -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
+ }
@@ -8,8 +8,9 @@ import type { AgentContextContent, AgentContextItem, AgentContextSource } from '
8
8
  * (useTool, useNotis, etc.) which read from the NotisProvider context.
9
9
  */
10
10
 
11
- import type { ComponentType } from 'react';
11
+ import type { ComponentType, ReactNode } from 'react';
12
12
  import type { NotisQueryClient } from './queryCache';
13
+ import type { SpaceRuntimeDescriptor, SpaceActionOptions, SpaceDocumentBodyRequest, SpaceDocumentBodyResult, SpaceViewerReadOperation, SpaceShownOptions, SpaceCloudComputerTransport } from './space';
13
14
 
14
15
  // ---------------------------------------------------------------------------
15
16
  // Database types
@@ -77,6 +78,8 @@ export type DocumentContentType = 'markdown' | 'file' | 'view';
77
78
 
78
79
  export interface DocumentRecord {
79
80
  id: string;
81
+ recordKey?: string;
82
+ revision?: number;
80
83
  title: string;
81
84
  properties: Record<string, unknown>;
82
85
  url?: string | null;
@@ -176,6 +179,14 @@ export interface QueryFilter {
176
179
  }
177
180
 
178
181
  export interface NotisRuntimeContext {
182
+ /** Host UI language. */
183
+ locale?: 'en' | 'fr';
184
+ /** Untrusted UI selection, separate from the host's fixed record authority. */
185
+ selection?: string | null;
186
+ /** Raw URL query values; `useViewParams()` keeps only the declared, valid ones. */
187
+ params?: Record<string, string> | null;
188
+ /** Trusted anonymous record-Site pin; no credentials or physical native target. */
189
+ siteRecord?: { recordKey: string; param: string; params: Record<string, string | number | boolean> } | null;
179
190
  collectionItem?: CollectionItemDetail | null;
180
191
  /** App-owned resource requested through the route's `?resource=<id>` link. */
181
192
  resourceId?: string | null;
@@ -230,14 +241,111 @@ export interface ContextSelection extends AgentContextContent {
230
241
  * file -> matching viewer/editor), so this contract stays stable as new
231
242
  * content types ship.
232
243
  */
233
- export interface NotisDocumentEditorProps {
234
- documentId: string;
244
+ export interface NotisRecordProps {
245
+ /** Stable native record key, authorized through this Space's declared databases. */
246
+ recordKey: string;
247
+ className?: string;
248
+ }
249
+
250
+ export type NotisDocumentEditorProps = ({ recordKey: string; documentId?: never } | {
251
+ /** Legacy Apps only. Spaces use recordKey. */
252
+ documentId: string; recordKey?: never;
253
+ }) & {
235
254
  /** 'full' renders icon/title/cover above the content; 'body' renders content only. */
236
255
  variant?: 'full' | 'body';
256
+ /** Show the database properties above the body (default true). */
257
+ showProperties?: boolean;
258
+ readOnly?: boolean;
259
+ className?: string;
260
+ onDirtyChange?: (dirty: boolean) => void;
261
+ onSavingChange?: (saving: boolean) => void;
262
+ }
263
+
264
+ export interface NotisRecordPropertiesProps extends NotisRecordProps {
237
265
  readOnly?: boolean;
266
+ onSavingChange?: (saving: boolean) => void;
267
+ }
268
+
269
+ /** Inline HTML is useful for declared Site read actions; recordKey loads a signed-in record. */
270
+ export type NotisHtmlFrameProps = ({ recordKey: string; html?: never } | { html: string; recordKey?: never }) & {
271
+ title?: string;
238
272
  className?: string;
273
+ };
274
+ export interface NotisReportFrameProps extends NotisRecordProps {}
275
+ export interface NotisShareControlProps extends NotisRecordProps {}
276
+
277
+ /** One breadcrumb before the record title; the title is always the last crumb. */
278
+ export interface NotisDocumentPageCrumb {
279
+ label: string;
280
+ onSelect?: () => void;
281
+ }
282
+
283
+ /** An extra entry in the page's '...' menu, after the host's own entries. */
284
+ export interface NotisDocumentPageMenuItem {
285
+ label: string;
286
+ onSelect: () => void;
287
+ destructive?: boolean;
288
+ disabled?: boolean;
289
+ }
290
+
291
+ /**
292
+ * The host-bound parts of one document page. They share one editor session and one save queue,
293
+ * so a Space can arrange them (for example the properties in a right column) without a second editor.
294
+ */
295
+ export interface NotisDocumentPageParts {
296
+ /** Breadcrumb, Saving or Saved status, actions, Share and the '...' menu. */
297
+ Toolbar: ComponentType;
298
+ /** Cover, icon and title. */
299
+ Header: ComponentType;
300
+ /** Editable or Read only, type, database, created and updated, with the Configure panel toggle. */
301
+ Meta: ComponentType;
302
+ /** The record's properties, saved automatically with the title, icon and cover. */
303
+ Properties: ComponentType;
304
+ /** The collaborative body (or the matching viewer for files, HTML and reports). */
305
+ Body: ComponentType;
306
+ }
307
+
308
+ /**
309
+ * The full page for one record that is the main content of a Space view (a note, a document):
310
+ * the same page Beta's document page showed, inside the Space. Every edit (title, icon, cover,
311
+ * properties, body) goes through the host's single save queue and live collaboration.
312
+ */
313
+ export interface NotisDocumentPageProps extends NotisRecordProps {
314
+ /** Crumbs before the record title, usually a way back to the list. `false` hides the breadcrumb. */
315
+ breadcrumb?: NotisDocumentPageCrumb[] | false;
316
+ /** Extra toolbar buttons, before Share and the '...' menu (for example the Space's own Trash button). */
317
+ actions?: ReactNode;
318
+ /** Extra '...' menu entries (for example a destructive Delete, as Beta's page had). Plain toolbar buttons on hosts without DocumentPage. */
319
+ menuItems?: NotisDocumentPageMenuItem[];
320
+ /** Show the host Share control (a record Site). Off by default: only Spaces whose records were shared on Beta turn it on. */
321
+ share?: boolean;
322
+ /** Which header parts show. `false` hides the whole header (cover, icon and title). */
323
+ header?: false | { cover?: boolean; icon?: boolean; title?: boolean };
324
+ /** Show the meta strip (Editable or Read only, type, database, dates). Default true. */
325
+ meta?: boolean;
326
+ /** 'panel' (default): properties in the Configure panel under the meta strip. 'hidden': none. Or choose and order them by property name. */
327
+ properties?: 'panel' | 'hidden' | { include?: string[]; exclude?: string[]; order?: string[] };
328
+ /** Rendered between the properties and the body. */
329
+ aboveBody?: ReactNode;
330
+ /** Rendered under the body. */
331
+ belowBody?: ReactNode;
332
+ /** Column width: standard (900px, as Beta), wide (1200px) or full. */
333
+ width?: 'standard' | 'wide' | 'full';
334
+ readOnly?: boolean;
239
335
  onDirtyChange?: (dirty: boolean) => void;
240
336
  onSavingChange?: (saving: boolean) => void;
337
+ /**
338
+ * Called after a title, icon, cover, property or body save is confirmed, so the Space can refresh its lists.
339
+ * On hosts without DocumentPage it is called when a save finishes, with `title` null.
340
+ */
341
+ onSaved?: (record: { recordKey: string; title: string | null }) => void;
342
+ /**
343
+ * Rearrange the page. Receives the host-bound parts; the default layout is Toolbar, Header, Meta (with
344
+ * Properties in its panel), Body. With a layout, Meta shows only its strip and Properties shows wherever
345
+ * the layout places it (once), so the record's properties are never edited from two places.
346
+ * aboveBody and belowBody are not drawn: place them in the layout.
347
+ */
348
+ layout?: (parts: NotisDocumentPageParts) => ReactNode;
241
349
  }
242
350
 
243
351
  export interface NotisMarkdownEditorSavePayload {
@@ -279,7 +387,14 @@ export interface NotisMarkdownEditorProps {
279
387
  */
280
388
  export interface NotisRuntimeUI {
281
389
  DocumentEditor?: ComponentType<NotisDocumentEditorProps>;
390
+ DocumentPage?: ComponentType<NotisDocumentPageProps>;
282
391
  MarkdownEditor?: ComponentType<NotisMarkdownEditorProps>;
392
+ RecordProperties?: ComponentType<NotisRecordPropertiesProps>;
393
+ HtmlFrame?: ComponentType<NotisHtmlFrameProps>;
394
+ ReportFrame?: ComponentType<NotisReportFrameProps>;
395
+ ShareControl?: ComponentType<NotisShareControlProps>;
396
+ /** Starts reading a record (and the page's code) before it opens, for example when a list row is hovered. */
397
+ prefetchRecord?: (recordKey: string) => void;
283
398
  }
284
399
 
285
400
  // ---------------------------------------------------------------------------
@@ -305,10 +420,12 @@ export interface HandoverPayload {
305
420
  /** Optional starter message. Omit it to open a context-only composer. */
306
421
  prompt?: string;
307
422
  /**
308
- * Key of a skill declared in `notis.config.ts` -> `skills[].key`. The host
309
- * rejects a key this app does not declare. Omit to hand over plain work.
423
+ * Alias of a Skill this Space links (`skills/<alias>/` in its source). The
424
+ * host rejects an alias the Space does not link. Omit to hand over plain work.
310
425
  */
311
426
  skill?: string;
427
+ /** Space-only selected row context; never overrides the host's access scope. */
428
+ record?: { binding: string; recordKey: string; readAction: string };
312
429
  /**
313
430
  * Submit on an active user gesture when the host supports immediate handover;
314
431
  * background calls still open an editable draft.
@@ -364,7 +481,7 @@ export interface CloudComputerFacts {
364
481
  }
365
482
 
366
483
  export interface RuntimeResource {
367
- kind: 'app' | 'report';
484
+ kind: 'app' | 'report' | 'space';
368
485
  /** Host-provided adoption eligibility, never report-authored authority. */
369
486
  analytics_eligible?: boolean;
370
487
  /** Host-derived report family for adoption analytics, e.g. skill review reports. */
@@ -377,6 +494,26 @@ export interface RuntimeResource {
377
494
  }
378
495
 
379
496
  export interface NotisRuntime {
497
+ space?: SpaceRuntimeDescriptor;
498
+ callAction?<TResult = unknown>(actionId: string, inputs?: Record<string, unknown>, options?: SpaceActionOptions): Promise<TResult>;
499
+ documentBody?(request: SpaceDocumentBodyRequest): Promise<SpaceDocumentBodyResult>;
500
+ /**
501
+ * Space-only declared viewer read (`viewerReads`): what the signed-in viewer can open,
502
+ * with their own authority. Absent on Sites, anonymous links and older hosts.
503
+ */
504
+ viewerRead?<TResult = unknown>(operation: SpaceViewerReadOperation, input?: Record<string, unknown>): Promise<TResult>;
505
+ /**
506
+ * Space-only V4 list (`shows`): runs exactly the declared filter with these params as the
507
+ * signed-in Editor. Absent on Sites and older hosts.
508
+ */
509
+ shown?<TResult = unknown>(name: string, params?: Record<string, unknown>, options?: SpaceShownOptions): Promise<TResult>;
510
+ /** Space-only unsent Manager draft; distinct from legacy App activation. */
511
+ draftHandover?(payload: HandoverPayload): Promise<HandoverResult>;
512
+ /**
513
+ * Space-only cloud computer (`cloudComputer`): the signed-in viewer's own cloud computer after that
514
+ * viewer allows it. Absent on Sites, links, verification harnesses and older hosts.
515
+ */
516
+ spaceCloudComputer?: SpaceCloudComputerTransport;
380
517
  /** Authenticated runtime identity; app below is presentation metadata only. */
381
518
  resource?: RuntimeResource;
382
519
  /** Optional host-scoped in-memory read cache. Older hosts remain supported. */
@@ -403,7 +540,8 @@ export interface NotisRuntime {
403
540
  * The change notification is only a signal — it carries no rows. Consumers
404
541
  * react by refetching through the normal tool path, so app scoping,
405
542
  * permissions and billing are unchanged. Use the `useDatabaseSubscription`
406
- * hook rather than calling this directly.
543
+ * hook rather than calling this directly. In a Space, `slug` is the database
544
+ * binding alias and the hook is `useNativeDocuments(alias, { subscribe: true })`.
407
545
  */
408
546
  subscribeDatabase?(
409
547
  slug: string,
@@ -436,7 +574,7 @@ export interface NotisRuntime {
436
574
  */
437
575
  cloudComputerFacts?(options?: { refresh?: boolean }): Promise<CloudComputerFacts>;
438
576
 
439
- navigate?: (payload: { kind: string; [key: string]: unknown }) => void;
577
+ navigate?: (payload: { kind: string; [key: string]: unknown }) => void | Promise<void>;
440
578
 
441
579
  registerTopBarSearch?: (
442
580
  config: