@notis_ai/cli 0.2.0-beta.178.1 → 0.2.0-beta.1792022492092.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 (124) hide show
  1. package/README.md +548 -98
  2. package/config/portal-route-roots.json +47 -0
  3. package/dist/agent-hooks/notis-agent-hook.mjs +32901 -5934
  4. package/dist/base-skills/notis-apps/SKILL.md +76 -51
  5. package/dist/base-skills/notis-apps/references/architecture.md +116 -164
  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 +105 -96
  10. package/dist/base-skills/notis-apps/references/sdk.md +115 -60
  11. package/dist/base-skills/notis-apps/references/troubleshooting.md +22 -21
  12. package/dist/base-skills/notis-apps/references/views.md +145 -0
  13. package/dist/base-skills/notis-cli/SKILL.md +29 -12
  14. package/dist/base-skills/notis-cli/references/app-delivery.md +12 -11
  15. package/dist/base-skills/notis-cli/references/intelligence.md +94 -0
  16. package/dist/base-skills/notis-cli/references/native-databases.md +6 -5
  17. package/dist/base-skills/notis-cli/references/tool-examples.md +4 -4
  18. package/dist/base-skills/notis-query/SKILL.md +34 -60
  19. package/dist/base-skills/notis-query/references/database-discovery.md +60 -59
  20. package/dist/base-skills/notis-query/references/documents.md +53 -39
  21. package/dist/base-skills/notis-query/references/query.md +57 -524
  22. package/dist/sdk/src/bundleModule.ts +79 -0
  23. package/dist/sdk/src/components/DocumentEditor.tsx +6 -3
  24. package/dist/sdk/src/components/DocumentPage.tsx +144 -0
  25. package/dist/sdk/src/components/RecordComponents.tsx +33 -0
  26. package/dist/sdk/src/components/RenderChartData.tsx +19 -0
  27. package/dist/sdk/src/config.ts +4 -26
  28. package/dist/sdk/src/documents.ts +2 -0
  29. package/dist/sdk/src/hooks/useAction.ts +28 -0
  30. package/dist/sdk/src/hooks/useBackend.ts +3 -2
  31. package/dist/sdk/src/hooks/useCloudComputer.ts +35 -4
  32. package/dist/sdk/src/hooks/useCloudComputerShell.ts +114 -0
  33. package/dist/sdk/src/hooks/useCollectionInteractions.ts +4 -2
  34. package/dist/sdk/src/hooks/useHandover.ts +7 -7
  35. package/dist/sdk/src/hooks/useNativeDocumentBody.ts +50 -0
  36. package/dist/sdk/src/hooks/useNativeDocuments.ts +83 -0
  37. package/dist/sdk/src/hooks/useNotis.ts +3 -0
  38. package/dist/sdk/src/hooks/useNotisNavigation.ts +26 -11
  39. package/dist/sdk/src/hooks/useQuery.ts +7 -4
  40. package/dist/sdk/src/hooks/useViewParams.ts +121 -0
  41. package/dist/sdk/src/hooks/useViewerReads.ts +129 -0
  42. package/dist/sdk/src/index.ts +36 -0
  43. package/dist/sdk/src/interactions/shortcuts.tsx +42 -5
  44. package/dist/sdk/src/nativeData.ts +39 -0
  45. package/dist/sdk/src/presentation.tsx +82 -0
  46. package/dist/sdk/src/provider.tsx +9 -4
  47. package/dist/sdk/src/queryCache.ts +5 -3
  48. package/dist/sdk/src/queryErrors.ts +51 -0
  49. package/dist/sdk/src/runtime.ts +163 -13
  50. package/dist/sdk/src/space.ts +425 -0
  51. package/dist/sdk/src/styles.css +96 -72
  52. package/dist/sdk/src/vite.ts +11 -1
  53. package/dist/skill-sync/index.js +571 -80
  54. package/dist/skill-sync/index.js.map +4 -4
  55. package/dist/skill-sync-worker.mjs +858 -233
  56. package/dist/space-harness/host.js +66 -0
  57. package/dist/view-renderer/dist/host.css +1 -0
  58. package/dist/view-renderer/dist/host.js +1028 -0
  59. package/dist/view-renderer/dist/theme.css +73 -0
  60. package/dist/view-renderer/src/dom.js +180 -0
  61. package/dist/view-renderer/src/errors.js +8 -0
  62. package/dist/view-renderer/src/host.tsx +175 -0
  63. package/dist/view-renderer/src/index.js +4 -0
  64. package/dist/view-renderer/src/pool.js +76 -0
  65. package/dist/view-renderer/src/remote-broker.js +59 -0
  66. package/dist/view-renderer/src/renderer.js +201 -0
  67. package/package.json +16 -5
  68. package/skills/notis-apps/cli.md +323 -163
  69. package/skills/notis-cli/AGENT_INSTRUCTIONS.md +3 -3
  70. package/skills/notis-onboarding/BRIEF.md +7 -3
  71. package/src/command-specs/apps.js +2 -1
  72. package/src/command-specs/auth.js +2 -2
  73. package/src/command-specs/files.js +29 -0
  74. package/src/command-specs/handover.js +331 -79
  75. package/src/command-specs/helpers.js +2 -0
  76. package/src/command-specs/index.js +11 -5
  77. package/src/command-specs/profile.js +42 -1
  78. package/src/command-specs/skills.js +242 -1
  79. package/src/command-specs/spaces.js +645 -0
  80. package/src/command-specs/tools.js +0 -6
  81. package/src/command-specs/views.js +69 -0
  82. package/src/runtime/agent-browser.js +92 -29
  83. package/src/runtime/app-boundary-validator.js +7 -4
  84. package/src/runtime/app-data-literals.js +82 -0
  85. package/src/runtime/app-platform.js +321 -253
  86. package/src/runtime/computer-identity.d.ts +30 -0
  87. package/src/runtime/computer-identity.js +151 -0
  88. package/src/runtime/config-lock.js +135 -0
  89. package/src/runtime/confined-space-build.js +74 -0
  90. package/src/runtime/confined-space-verify.js +49 -0
  91. package/src/runtime/delegated-context.js +5 -21
  92. package/src/runtime/native-record-files.js +47 -0
  93. package/src/runtime/oauth.js +18 -1
  94. package/src/runtime/profiles.js +5 -107
  95. package/src/runtime/retired-sdk-files.js +20 -0
  96. package/src/runtime/screenshot-framing.js +85 -0
  97. package/src/runtime/skill-sync/cloud-client.ts +26 -4
  98. package/src/runtime/skill-sync/index.ts +230 -35
  99. package/src/runtime/skill-sync/local-scanner.ts +188 -29
  100. package/src/runtime/skill-sync/native-identity.ts +102 -0
  101. package/src/runtime/skill-sync/native-plan.ts +70 -0
  102. package/src/runtime/skill-sync/sync-plan.ts +16 -9
  103. package/src/runtime/skill-sync/types.ts +32 -6
  104. package/src/runtime/source-root.js +32 -0
  105. package/src/runtime/space-collection-scaffold.js +66 -0
  106. package/src/runtime/space-database-structures.js +229 -0
  107. package/src/runtime/space-fixture-broker.js +50 -0
  108. package/src/runtime/space-lock.js +337 -0
  109. package/src/runtime/space-platform.js +105 -0
  110. package/src/runtime/space-source.js +281 -0
  111. package/src/runtime/space-test-server.js +327 -0
  112. package/src/runtime/space-verification.js +89 -0
  113. package/src/runtime/space-verify-browser-worker.js +51 -0
  114. package/src/runtime/space-view-manifest.js +254 -0
  115. package/src/runtime/store-screenshot.js +10 -7
  116. package/src/runtime/sync-skills.d.ts +3 -1
  117. package/src/runtime/sync-skills.js +10 -2
  118. package/src/runtime/transport.js +8 -6
  119. package/src/runtime/view-render.js +49 -0
  120. package/src/skill-sync-worker.js +14 -2
  121. package/src/space-harness-host.ts +103 -0
  122. package/template/.harness/index.html.tmpl +7 -10
  123. package/template/app/globals.css +1 -0
  124. package/src/command-specs/reports.js +0 -97
@@ -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
+ }
@@ -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,119 @@ 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
+ /**
321
+ * Offer the record's Site: a row of its Space view's top bar Share panel, or a toolbar Share when the page shows in
322
+ * another Space's view. Off by default: only Spaces whose records were shared on Beta turn it on.
323
+ */
324
+ share?: boolean;
325
+ /** Which header parts show. `false` hides the whole header (cover, icon and title). */
326
+ header?: false | { cover?: boolean; icon?: boolean; title?: boolean };
327
+ /** Show the meta strip (Editable or Read only, type, database, dates). Default true. */
328
+ meta?: boolean;
329
+ /** 'panel' (default): properties in the Configure panel under the meta strip. 'hidden': none. Or choose and order them by property name. */
330
+ properties?: 'panel' | 'hidden' | { include?: string[]; exclude?: string[]; order?: string[] };
331
+ /** Rendered between the properties and the body. */
332
+ aboveBody?: ReactNode;
333
+ /** Rendered under the body. */
334
+ belowBody?: ReactNode;
335
+ /** Column width: standard (900px, as Beta), wide (1200px) or full. */
336
+ width?: 'standard' | 'wide' | 'full';
337
+ /**
338
+ * 'html': the record's one stored `.html` file is the page. It opens full height under the toolbar,
339
+ * like an HTML record, without the header, meta, properties or body editor.
340
+ */
341
+ viewer?: 'html';
342
+ readOnly?: boolean;
239
343
  onDirtyChange?: (dirty: boolean) => void;
240
344
  onSavingChange?: (saving: boolean) => void;
345
+ /**
346
+ * Called after a title, icon, cover, property or body save is confirmed, so the Space can refresh its lists.
347
+ * On hosts without DocumentPage it is called when a save finishes, with `title` null.
348
+ */
349
+ onSaved?: (record: { recordKey: string; title: string | null }) => void;
350
+ /**
351
+ * Rearrange the page. Receives the host-bound parts; the default layout is Toolbar, Header, Meta (with
352
+ * Properties in its panel), Body. With a layout, Meta shows only its strip and Properties shows wherever
353
+ * the layout places it (once), so the record's properties are never edited from two places.
354
+ * aboveBody and belowBody are not drawn: place them in the layout.
355
+ */
356
+ layout?: (parts: NotisDocumentPageParts) => ReactNode;
241
357
  }
242
358
 
243
359
  export interface NotisMarkdownEditorSavePayload {
@@ -279,7 +395,14 @@ export interface NotisMarkdownEditorProps {
279
395
  */
280
396
  export interface NotisRuntimeUI {
281
397
  DocumentEditor?: ComponentType<NotisDocumentEditorProps>;
398
+ DocumentPage?: ComponentType<NotisDocumentPageProps>;
282
399
  MarkdownEditor?: ComponentType<NotisMarkdownEditorProps>;
400
+ RecordProperties?: ComponentType<NotisRecordPropertiesProps>;
401
+ HtmlFrame?: ComponentType<NotisHtmlFrameProps>;
402
+ ReportFrame?: ComponentType<NotisReportFrameProps>;
403
+ ShareControl?: ComponentType<NotisShareControlProps>;
404
+ /** Starts reading a record (and the page's code) before it opens, for example when a list row is hovered. */
405
+ prefetchRecord?: (recordKey: string) => void;
283
406
  }
284
407
 
285
408
  // ---------------------------------------------------------------------------
@@ -305,14 +428,15 @@ export interface HandoverPayload {
305
428
  /** Optional starter message. Omit it to open a context-only composer. */
306
429
  prompt?: string;
307
430
  /**
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.
431
+ * Alias of a Skill this Space links (`skills/<alias>/` in its source). The
432
+ * host rejects an alias the Space does not link. Omit to hand over plain work.
310
433
  */
311
434
  skill?: string;
435
+ /** Space-only selected row context; never overrides the host's access scope. */
436
+ record?: { binding: string; recordKey: string; readAction: string };
312
437
  /**
313
- * Accepted for forward compatibility. The portal never submits a composer on
314
- * the user's behalf today, so every handover resolves `drafted`; `sent` is
315
- * reserved for a host that can genuinely dispatch the run.
438
+ * Submit on an active user gesture when the host supports immediate handover;
439
+ * background calls still open an editable draft.
316
440
  */
317
441
  autoSend?: boolean;
318
442
  }
@@ -326,8 +450,8 @@ export interface HandoverResult {
326
450
  * The user's cloud computer, as far as an app may see it.
327
451
  *
328
452
  * `exists` is false when the user has never had a sandbox provisioned. A
329
- * `status` of anything other than `'running'` means the VM is asleep; reading
330
- * these facts never wakes it.
453
+ * `status` is the last observed VM state; `freshness` and `observed_at` describe
454
+ * its age. Reading these facts never wakes the VM.
331
455
  */
332
456
  export interface CloudComputerSandboxFacts {
333
457
  exists: boolean;
@@ -335,6 +459,8 @@ export interface CloudComputerSandboxFacts {
335
459
  provider: string | null;
336
460
  created_at: string | null;
337
461
  updated_at: string | null;
462
+ observed_at?: string | null;
463
+ freshness?: 'fresh' | 'stale' | 'unknown';
338
464
  }
339
465
 
340
466
  /**
@@ -349,6 +475,7 @@ export interface CloudComputerCliAuthFacts {
349
475
  authenticated: boolean | null;
350
476
  account: string | null;
351
477
  checked_at: string | null;
478
+ freshness?: 'fresh' | 'stale' | 'unknown';
352
479
  reason: string | null;
353
480
  }
354
481
 
@@ -365,9 +492,11 @@ export interface CloudComputerFacts {
365
492
  }
366
493
 
367
494
  export interface RuntimeResource {
368
- kind: 'app' | 'report';
495
+ kind: 'app' | 'report' | 'space';
369
496
  /** Host-provided adoption eligibility, never report-authored authority. */
370
497
  analytics_eligible?: boolean;
498
+ /** Host-derived report family for adoption analytics, e.g. skill review reports. */
499
+ report_kind?: 'skill_review' | 'custom';
371
500
  id: string;
372
501
  revision: number;
373
502
  name?: string;
@@ -376,6 +505,26 @@ export interface RuntimeResource {
376
505
  }
377
506
 
378
507
  export interface NotisRuntime {
508
+ space?: SpaceRuntimeDescriptor;
509
+ callAction?<TResult = unknown>(actionId: string, inputs?: Record<string, unknown>, options?: SpaceActionOptions): Promise<TResult>;
510
+ documentBody?(request: SpaceDocumentBodyRequest): Promise<SpaceDocumentBodyResult>;
511
+ /**
512
+ * Space-only declared viewer read (`viewerReads`): what the signed-in viewer can open,
513
+ * with their own authority. Absent on Sites, anonymous links and older hosts.
514
+ */
515
+ viewerRead?<TResult = unknown>(operation: SpaceViewerReadOperation, input?: Record<string, unknown>): Promise<TResult>;
516
+ /**
517
+ * Space-only V4 list (`shows`): runs exactly the declared filter with these params as the
518
+ * signed-in Editor. Absent on Sites and older hosts.
519
+ */
520
+ shown?<TResult = unknown>(name: string, params?: Record<string, unknown>, options?: SpaceShownOptions): Promise<TResult>;
521
+ /** Space-only unsent Manager draft; distinct from legacy App activation. */
522
+ draftHandover?(payload: HandoverPayload): Promise<HandoverResult>;
523
+ /**
524
+ * Space-only cloud computer (`cloudComputer`): the signed-in viewer's own cloud computer after that
525
+ * viewer allows it. Absent on Sites, links, verification harnesses and older hosts.
526
+ */
527
+ spaceCloudComputer?: SpaceCloudComputerTransport;
379
528
  /** Authenticated runtime identity; app below is presentation metadata only. */
380
529
  resource?: RuntimeResource;
381
530
  /** Optional host-scoped in-memory read cache. Older hosts remain supported. */
@@ -402,7 +551,8 @@ export interface NotisRuntime {
402
551
  * The change notification is only a signal — it carries no rows. Consumers
403
552
  * react by refetching through the normal tool path, so app scoping,
404
553
  * permissions and billing are unchanged. Use the `useDatabaseSubscription`
405
- * hook rather than calling this directly.
554
+ * hook rather than calling this directly. In a Space, `slug` is the database
555
+ * binding alias and the hook is `useNativeDocuments(alias, { subscribe: true })`.
406
556
  */
407
557
  subscribeDatabase?(
408
558
  slug: string,
@@ -435,7 +585,7 @@ export interface NotisRuntime {
435
585
  */
436
586
  cloudComputerFacts?(options?: { refresh?: boolean }): Promise<CloudComputerFacts>;
437
587
 
438
- navigate?: (payload: { kind: string; [key: string]: unknown }) => void;
588
+ navigate?: (payload: { kind: string; [key: string]: unknown }) => void | Promise<void>;
439
589
 
440
590
  registerTopBarSearch?: (
441
591
  config: