@notis_ai/cli 0.2.0-beta.16.1 → 0.2.0-beta.161.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 +435 -133
- 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/dist/agent-hooks/notis-agent-hook.mjs +19008 -0
- package/dist/base-skills/notis-apps/SKILL.md +70 -0
- package/dist/base-skills/notis-apps/references/architecture.md +164 -0
- package/dist/base-skills/notis-apps/references/context.md +81 -0
- package/dist/base-skills/notis-apps/references/design.md +165 -0
- package/dist/base-skills/notis-apps/references/reading.md +89 -0
- package/dist/base-skills/notis-apps/references/release.md +99 -0
- package/dist/base-skills/notis-apps/references/sdk.md +62 -0
- package/dist/base-skills/notis-apps/references/troubleshooting.md +23 -0
- package/dist/base-skills/notis-cli/SKILL.md +140 -0
- package/dist/base-skills/notis-cli/references/app-delivery.md +18 -0
- package/dist/base-skills/notis-cli/references/native-databases.md +20 -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 +67 -0
- package/dist/base-skills/notis-query/references/database-discovery.md +59 -0
- package/dist/base-skills/notis-query/references/documents.md +50 -0
- package/dist/base-skills/notis-query/references/query.md +543 -0
- package/dist/skill-sync/index.js +1626 -0
- package/dist/skill-sync/index.js.map +7 -0
- package/dist/skill-sync-worker.mjs +2990 -0
- package/package.json +18 -7
- package/skills/notis-apps/cli.md +313 -0
- package/skills/notis-cli/AGENT_INSTRUCTIONS.md +39 -0
- package/skills/notis-onboarding/BRIEF.md +129 -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 +1470 -202
- package/src/command-specs/auth.js +114 -137
- package/src/command-specs/diagnostics.js +729 -0
- package/src/command-specs/handover.js +374 -0
- package/src/command-specs/helpers.js +84 -82
- package/src/command-specs/index.js +25 -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/reports.js +86 -0
- package/src/command-specs/skills.js +75 -0
- package/src/command-specs/smoke.js +386 -0
- package/src/command-specs/tools.js +455 -139
- package/src/runtime/agent-browser.js +632 -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 +404 -0
- package/src/runtime/app-changelog.js +79 -0
- package/src/runtime/app-platform.js +2633 -210
- package/src/runtime/app-registry-scaffolds.js +367 -0
- package/src/runtime/app-test-server.js +292 -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/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/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/skill-sync/cloud-client.ts +99 -0
- package/src/runtime/skill-sync/index.ts +697 -0
- package/src/runtime/skill-sync/local-scanner.ts +1046 -0
- package/src/runtime/skill-sync/symlink-manager.ts +433 -0
- package/src/runtime/skill-sync/sync-plan.ts +22 -0
- package/src/runtime/skill-sync/types.ts +110 -0
- package/src/runtime/skill-sync/write-cloud-skill.ts +50 -0
- package/src/runtime/skill-sync-service.js +109 -0
- package/src/runtime/store-screenshot.js +143 -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 +324 -45
- package/src/skill-sync-worker-entry.js +2 -0
- package/src/skill-sync-worker.js +50 -0
- package/template/.harness/index.html.tmpl +430 -0
- package/template/CHANGELOG.md +5 -0
- package/template/app/globals.css +28 -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/packages/{notis-sdk → sdk}/package.json +14 -4
- package/template/packages/sdk/src/agentContext.ts +36 -0
- package/template/packages/sdk/src/components/DocumentEditor.tsx +103 -0
- package/template/packages/sdk/src/components/Markdown.tsx +60 -0
- package/template/packages/sdk/src/components/MarkdownEditor.tsx +121 -0
- package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +285 -0
- package/template/packages/sdk/src/components/MultiSelectCheckbox.tsx +97 -0
- package/template/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/template/packages/sdk/src/components/NotisCommentBoundary.tsx +172 -0
- package/template/packages/sdk/src/components/NotisSelectionBoundary.tsx +59 -0
- package/template/packages/sdk/src/components/ShortcutHints.tsx +56 -0
- package/template/packages/sdk/src/components/Skeleton.tsx +24 -0
- package/template/packages/sdk/src/config.ts +257 -0
- package/template/packages/sdk/src/documents.ts +256 -0
- package/template/packages/sdk/src/hooks/useActiveResource.ts +19 -0
- package/template/packages/sdk/src/hooks/useAgentContext.ts +23 -0
- package/template/packages/sdk/src/hooks/useCloudComputer.ts +64 -0
- package/template/packages/sdk/src/hooks/useCollectionInteractions.ts +836 -0
- package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +49 -0
- package/template/packages/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
- package/template/packages/sdk/src/hooks/useDocument.ts +43 -0
- package/template/packages/sdk/src/hooks/useDocuments.ts +84 -0
- package/template/packages/sdk/src/hooks/useHandover.ts +78 -0
- package/template/packages/sdk/src/hooks/useLongPressSelection.ts +79 -0
- package/template/packages/sdk/src/hooks/useMultiSelect.ts +95 -0
- package/template/packages/{notis-sdk → sdk}/src/hooks/useNotis.ts +10 -4
- package/template/packages/{notis-sdk → sdk}/src/hooks/useNotisNavigation.ts +11 -8
- package/template/packages/sdk/src/hooks/useQuery.ts +71 -0
- package/template/packages/sdk/src/hooks/useTool.ts +65 -0
- package/template/packages/sdk/src/hooks/useToolQuery.ts +12 -0
- package/template/packages/sdk/src/hooks/useTopBarSearch.ts +81 -0
- package/template/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/template/packages/sdk/src/index.ts +161 -0
- package/template/packages/sdk/src/interactions/actions.ts +59 -0
- package/template/packages/sdk/src/interactions/shortcuts.tsx +694 -0
- package/template/packages/sdk/src/interactions/visibility.ts +13 -0
- package/template/packages/sdk/src/interactions.ts +45 -0
- package/template/packages/sdk/src/provider.tsx +44 -0
- package/template/packages/sdk/src/queryCache.ts +170 -0
- package/template/packages/sdk/src/runtime.ts +451 -0
- package/template/packages/sdk/src/styles.css +247 -0
- package/template/packages/sdk/src/tailwind.ts +66 -0
- package/template/packages/sdk/src/vite.ts +73 -0
- package/template/packages/{notis-sdk → sdk}/tsconfig.json +1 -0
- 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/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 → sdk}/src/hooks/useBackend.ts +0 -0
- /package/template/packages/{notis-sdk → sdk}/src/hooks/useTools.ts +0 -0
- /package/template/packages/{notis-sdk → sdk}/src/ui.ts +0 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useQuery } from './useQuery';
|
|
4
|
+
import { useNotisRuntime } from '../provider';
|
|
5
|
+
import { normalizeDatabaseProperty, optionalString } from '../documents';
|
|
6
|
+
import type { DatabaseProperty } from '../runtime';
|
|
7
|
+
|
|
8
|
+
interface GetDatabaseResult {
|
|
9
|
+
database?: {
|
|
10
|
+
name?: string;
|
|
11
|
+
description?: string | null;
|
|
12
|
+
schema?: {
|
|
13
|
+
properties?: unknown[];
|
|
14
|
+
};
|
|
15
|
+
};
|
|
16
|
+
message?: string;
|
|
17
|
+
error?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface UseDatabaseSchemaResult {
|
|
21
|
+
name: string | null;
|
|
22
|
+
description: string | null;
|
|
23
|
+
properties: DatabaseProperty[];
|
|
24
|
+
loading: boolean;
|
|
25
|
+
isFetching: boolean;
|
|
26
|
+
hasData: boolean;
|
|
27
|
+
error: Error | null;
|
|
28
|
+
refetch: () => void;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Fetch a database's schema (normalized properties + metadata). */
|
|
32
|
+
export function useDatabaseSchema(databaseSlug: string): UseDatabaseSchemaResult {
|
|
33
|
+
const runtime = useNotisRuntime();
|
|
34
|
+
const query = useQuery(['database-schema', databaseSlug], async () => {
|
|
35
|
+
const result = await runtime!.callTool<GetDatabaseResult>('LOCAL_NOTIS_DATABASE_GET_DATABASE', {
|
|
36
|
+
database_slug: databaseSlug,
|
|
37
|
+
}, { dedupe: true, readOnly: true });
|
|
38
|
+
if (!result.database) throw new Error(result.error || result.message || 'Database not found');
|
|
39
|
+
return {
|
|
40
|
+
name: optionalString(result.database.name),
|
|
41
|
+
description: optionalString(result.database.description),
|
|
42
|
+
properties: (result.database.schema?.properties ?? []).map(normalizeDatabaseProperty)
|
|
43
|
+
.filter((property): property is DatabaseProperty => Boolean(property)),
|
|
44
|
+
};
|
|
45
|
+
}, { readOnly: true });
|
|
46
|
+
return { ...query, name: query.data?.name ?? null, description: query.data?.description ?? null,
|
|
47
|
+
properties: query.data?.properties ?? EMPTY_PROPERTIES };
|
|
48
|
+
}
|
|
49
|
+
const EMPTY_PROPERTIES: DatabaseProperty[] = [];
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { useNotisRuntime } from '../provider';
|
|
5
|
+
import { useDocuments, type UseDocumentsOptions, type UseDocumentsResult } from './useDocuments';
|
|
6
|
+
import type { DocumentRecord } from '../runtime';
|
|
7
|
+
|
|
8
|
+
export interface UseDatabaseSubscriptionOptions extends UseDocumentsOptions {
|
|
9
|
+
/** Set to false to keep the query but skip the change feed. */
|
|
10
|
+
subscribe?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface UseDatabaseSubscriptionResult extends UseDocumentsResult {
|
|
14
|
+
/** Alias of `documents`, for views that think in rows. */
|
|
15
|
+
rows: DocumentRecord[];
|
|
16
|
+
/** True while a live change feed is attached to this database. */
|
|
17
|
+
live: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Query a Notis database and keep it fresh without polling.
|
|
22
|
+
*
|
|
23
|
+
* ```tsx
|
|
24
|
+
* const { rows, live, refetch } = useDatabaseSubscription('workspaces');
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* A change on the database wakes the hook, which then refetches through the
|
|
28
|
+
* usual `LOCAL_NOTIS_DATABASE_QUERY` path — the change feed is a signal only
|
|
29
|
+
* and never carries row data. Hosts without a change feed (the dev harness,
|
|
30
|
+
* the screenshot stub, the vite preview) still return rows; `live` is false
|
|
31
|
+
* there and the app should keep offering its manual refresh.
|
|
32
|
+
*/
|
|
33
|
+
export function useDatabaseSubscription(
|
|
34
|
+
databaseSlug: string,
|
|
35
|
+
options: UseDatabaseSubscriptionOptions = {},
|
|
36
|
+
): UseDatabaseSubscriptionResult {
|
|
37
|
+
const runtime = useNotisRuntime();
|
|
38
|
+
const { subscribe = true, ...documentOptions } = options;
|
|
39
|
+
const { documents, loading, isFetching, hasData, error, refetch } = useDocuments(databaseSlug, documentOptions);
|
|
40
|
+
const [live, setLive] = useState(false);
|
|
41
|
+
|
|
42
|
+
const refetchRef = useRef(refetch);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
refetchRef.current = refetch;
|
|
45
|
+
}, [refetch]);
|
|
46
|
+
|
|
47
|
+
const enabled = options.enabled !== false && subscribe;
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!runtime?.subscribeDatabase || !enabled || !databaseSlug) {
|
|
51
|
+
setLive(false);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let cancelled = false;
|
|
56
|
+
const unsubscribe = runtime.subscribeDatabase(
|
|
57
|
+
databaseSlug,
|
|
58
|
+
() => {
|
|
59
|
+
refetchRef.current();
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
onStatusChange: (isLive) => {
|
|
63
|
+
if (!cancelled) setLive(isLive);
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return () => {
|
|
69
|
+
cancelled = true;
|
|
70
|
+
setLive(false);
|
|
71
|
+
unsubscribe?.();
|
|
72
|
+
};
|
|
73
|
+
}, [runtime, databaseSlug, enabled]);
|
|
74
|
+
|
|
75
|
+
return { documents, rows: documents, loading, isFetching, hasData, error, refetch, live };
|
|
76
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useQuery } from './useQuery';
|
|
4
|
+
import { useNotisRuntime } from '../provider';
|
|
5
|
+
import { normalizeDocumentRecord } from '../documents';
|
|
6
|
+
import type { DocumentRecord } from '../runtime';
|
|
7
|
+
|
|
8
|
+
interface GetDocumentResult {
|
|
9
|
+
status?: string;
|
|
10
|
+
document?: unknown;
|
|
11
|
+
message?: string;
|
|
12
|
+
error?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface UseDocumentOptions {
|
|
16
|
+
enabled?: boolean;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface UseDocumentResult {
|
|
20
|
+
document: DocumentRecord | null;
|
|
21
|
+
loading: boolean;
|
|
22
|
+
isFetching: boolean;
|
|
23
|
+
hasData: boolean;
|
|
24
|
+
error: Error | null;
|
|
25
|
+
refetch: () => void;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Fetch a single document (with content) by id, normalized. */
|
|
29
|
+
export function useDocument(
|
|
30
|
+
documentId: string | null | undefined,
|
|
31
|
+
options: UseDocumentOptions = {},
|
|
32
|
+
): UseDocumentResult {
|
|
33
|
+
const runtime = useNotisRuntime();
|
|
34
|
+
const query = useQuery<DocumentRecord>(['document', documentId ?? null], async () => {
|
|
35
|
+
if (!runtime || !documentId) throw new Error('Document not found');
|
|
36
|
+
const result = await runtime.callTool<GetDocumentResult>('LOCAL_NOTIS_DATABASE_GET_DOCUMENT', {
|
|
37
|
+
document_id: documentId,
|
|
38
|
+
}, { dedupe: true, readOnly: true });
|
|
39
|
+
if (!result.document) throw new Error(result.error ?? result.message ?? 'Document not found');
|
|
40
|
+
return normalizeDocumentRecord(result.document);
|
|
41
|
+
}, { readOnly: true, enabled: options.enabled !== false && Boolean(documentId) });
|
|
42
|
+
return { document: query.data ?? null, loading: query.loading, isFetching: query.isFetching, hasData: query.hasData, error: query.error, refetch: query.refetch };
|
|
43
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useQuery } from './useQuery';
|
|
4
|
+
import { useNotisRuntime } from '../provider';
|
|
5
|
+
import { normalizeDocumentRecord } from '../documents';
|
|
6
|
+
import type { DocumentRecord, QueryFilter } from '../runtime';
|
|
7
|
+
|
|
8
|
+
interface QueryDatabaseResult {
|
|
9
|
+
documents?: unknown[];
|
|
10
|
+
has_more?: boolean;
|
|
11
|
+
next_offset?: number | null;
|
|
12
|
+
message?: string;
|
|
13
|
+
error?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface UseDocumentsOptions {
|
|
17
|
+
filter?: QueryFilter;
|
|
18
|
+
pageSize?: number;
|
|
19
|
+
offset?: number;
|
|
20
|
+
/** Fetch every page, starting at offset, instead of returning only one page. */
|
|
21
|
+
fetchAll?: boolean;
|
|
22
|
+
/** App-view lists can omit bodies until opening/searching a document. Default: true. */
|
|
23
|
+
includeContent?: boolean;
|
|
24
|
+
enabled?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface UseDocumentsResult {
|
|
28
|
+
documents: DocumentRecord[];
|
|
29
|
+
loading: boolean;
|
|
30
|
+
isFetching: boolean;
|
|
31
|
+
hasData: boolean;
|
|
32
|
+
error: Error | null;
|
|
33
|
+
refetch: () => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Query a Notis database and get normalized `DocumentRecord`s.
|
|
38
|
+
*
|
|
39
|
+
* ```tsx
|
|
40
|
+
* const { documents, loading, refetch } = useDocuments('notes', {
|
|
41
|
+
* filter: { filters: [{ property: 'Folder', operator: 'contains', type: 'relation', value: folderId }] },
|
|
42
|
+
* pageSize: 250,
|
|
43
|
+
* });
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
export function useDocuments(
|
|
47
|
+
databaseSlug: string,
|
|
48
|
+
options: UseDocumentsOptions = {},
|
|
49
|
+
): UseDocumentsResult {
|
|
50
|
+
const runtime = useNotisRuntime();
|
|
51
|
+
const query = useQuery<DocumentRecord[]>(
|
|
52
|
+
['documents', databaseSlug, options.filter ?? null, options.pageSize ?? null, options.offset ?? 0, Boolean(options.fetchAll), options.includeContent !== false],
|
|
53
|
+
async () => {
|
|
54
|
+
if (!runtime) throw new Error('Notis runtime not available');
|
|
55
|
+
const allDocuments: unknown[] = [];
|
|
56
|
+
let offset = options.offset ?? 0;
|
|
57
|
+
while (true) {
|
|
58
|
+
const result = await runtime.callTool<QueryDatabaseResult>('LOCAL_NOTIS_DATABASE_QUERY', {
|
|
59
|
+
database_slug: databaseSlug,
|
|
60
|
+
query: {
|
|
61
|
+
...(options.filter ?? {}),
|
|
62
|
+
...(options.pageSize !== undefined ? { page_size: options.pageSize } : {}),
|
|
63
|
+
...(options.includeContent === false ? { include_content: false } : {}),
|
|
64
|
+
},
|
|
65
|
+
...(offset > 0 ? { offset } : {}),
|
|
66
|
+
}, { dedupe: true, readOnly: true });
|
|
67
|
+
const message = result.error ?? result.message;
|
|
68
|
+
if (!result.documents && message) throw new Error(message);
|
|
69
|
+
allDocuments.push(...(result.documents ?? []));
|
|
70
|
+
if (!options.fetchAll || !result.has_more) break;
|
|
71
|
+
const nextOffset = result.next_offset;
|
|
72
|
+
if (typeof nextOffset !== 'number' || nextOffset <= offset) {
|
|
73
|
+
throw new Error('Database query returned an invalid pagination offset');
|
|
74
|
+
}
|
|
75
|
+
offset = nextOffset;
|
|
76
|
+
}
|
|
77
|
+
return allDocuments.map(normalizeDocumentRecord).filter((document) => document.id);
|
|
78
|
+
},
|
|
79
|
+
{ readOnly: true, enabled: options.enabled },
|
|
80
|
+
);
|
|
81
|
+
return { documents: query.data ?? EMPTY_DOCUMENTS, loading: query.loading, isFetching: query.isFetching, hasData: query.hasData, error: query.error, refetch: query.refetch };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const EMPTY_DOCUMENTS: DocumentRecord[] = [];
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState } from 'react';
|
|
4
|
+
import { useNotisRuntime } from '../provider';
|
|
5
|
+
import type { HandoverPayload, HandoverResult } from '../runtime';
|
|
6
|
+
|
|
7
|
+
export interface UseHandoverResult {
|
|
8
|
+
/** Hand the work over. Rejects when the host has no manager chat. */
|
|
9
|
+
handover: (payload: HandoverPayload) => Promise<HandoverResult>;
|
|
10
|
+
/** True while the manager chat is being prepared. */
|
|
11
|
+
pending: boolean;
|
|
12
|
+
error: Error | null;
|
|
13
|
+
/**
|
|
14
|
+
* False when the host cannot hand work over (dev harness, vite preview).
|
|
15
|
+
* Render the app's own fallback — a copyable prompt, say — when it is false.
|
|
16
|
+
*/
|
|
17
|
+
available: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Hand a piece of work from app code to the Notis manager chat.
|
|
22
|
+
*
|
|
23
|
+
* An app displays work; the manager runs it. `handover` puts the message in
|
|
24
|
+
* the chat surface that already owns streaming progress, billing, cancellation
|
|
25
|
+
* and the transcript, and the app watches its own databases for the result.
|
|
26
|
+
*
|
|
27
|
+
* ```tsx
|
|
28
|
+
* const { handover, pending, available } = useHandover();
|
|
29
|
+
*
|
|
30
|
+
* return available ? (
|
|
31
|
+
* <Button
|
|
32
|
+
* disabled={pending}
|
|
33
|
+
* onClick={() => { void handover({ prompt: 'Create a workspace on notis to ...' }); }}
|
|
34
|
+
* >
|
|
35
|
+
* Send to Notis
|
|
36
|
+
* </Button>
|
|
37
|
+
* ) : (
|
|
38
|
+
* <CopyablePrompt prompt="Create a workspace on notis to ..." />
|
|
39
|
+
* );
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* Omit `prompt` to open a context-only composer, for example when a visible
|
|
43
|
+
* feedback button should let the user write in their own words.
|
|
44
|
+
*
|
|
45
|
+
* Pass `skill` to bind the work to a skill declared in `notis.config.ts`; the
|
|
46
|
+
* host rejects a key the app does not declare. `autoSend` is accepted for
|
|
47
|
+
* forward compatibility; today's hosts always return `drafted` and let the
|
|
48
|
+
* user press send.
|
|
49
|
+
*/
|
|
50
|
+
export function useHandover(): UseHandoverResult {
|
|
51
|
+
const runtime = useNotisRuntime();
|
|
52
|
+
const [pending, setPending] = useState(false);
|
|
53
|
+
const [error, setError] = useState<Error | null>(null);
|
|
54
|
+
|
|
55
|
+
const handover = useCallback(
|
|
56
|
+
async (payload: HandoverPayload): Promise<HandoverResult> => {
|
|
57
|
+
if (!runtime?.handover) {
|
|
58
|
+
throw new Error('This Notis host cannot hand work to the manager chat.');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
setPending(true);
|
|
62
|
+
setError(null);
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
return await runtime.handover(payload);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
68
|
+
setError(e);
|
|
69
|
+
throw e;
|
|
70
|
+
} finally {
|
|
71
|
+
setPending(false);
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
[runtime],
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
return { handover, pending, error, available: Boolean(runtime?.handover) };
|
|
78
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef } from 'react';
|
|
4
|
+
import type {
|
|
5
|
+
MouseEvent as ReactMouseEvent,
|
|
6
|
+
PointerEvent as ReactPointerEvent,
|
|
7
|
+
} from 'react';
|
|
8
|
+
|
|
9
|
+
const LONG_PRESS_MS = 500;
|
|
10
|
+
const MOVE_TOLERANCE_PX = 10;
|
|
11
|
+
const INTERACTIVE_SELECTOR = [
|
|
12
|
+
'button',
|
|
13
|
+
'a[href]',
|
|
14
|
+
'input',
|
|
15
|
+
'textarea',
|
|
16
|
+
'select',
|
|
17
|
+
'summary',
|
|
18
|
+
'[contenteditable]:not([contenteditable="false"])',
|
|
19
|
+
'[role="button"]',
|
|
20
|
+
'[role="link"]',
|
|
21
|
+
'[role="menuitem"]',
|
|
22
|
+
'[role="option"]',
|
|
23
|
+
'[role="switch"]',
|
|
24
|
+
'[role="tab"]',
|
|
25
|
+
].join(', ');
|
|
26
|
+
|
|
27
|
+
/** Adds a touch-only long-press entry point without rendering idle checkboxes. */
|
|
28
|
+
export function useLongPressSelection(onSelect: (id: string) => void) {
|
|
29
|
+
const onSelectRef = useRef(onSelect);
|
|
30
|
+
onSelectRef.current = onSelect;
|
|
31
|
+
const timerRef = useRef<number | null>(null);
|
|
32
|
+
const startRef = useRef<{ x: number; y: number } | null>(null);
|
|
33
|
+
const suppressClickRef = useRef(false);
|
|
34
|
+
|
|
35
|
+
const cancel = useCallback(() => {
|
|
36
|
+
if (timerRef.current !== null) window.clearTimeout(timerRef.current);
|
|
37
|
+
timerRef.current = null;
|
|
38
|
+
startRef.current = null;
|
|
39
|
+
}, []);
|
|
40
|
+
|
|
41
|
+
useEffect(() => cancel, [cancel]);
|
|
42
|
+
|
|
43
|
+
return useCallback((id: string) => ({
|
|
44
|
+
onPointerDown: (event: ReactPointerEvent<HTMLElement>) => {
|
|
45
|
+
if (event.pointerType !== 'touch' || event.button !== 0) return;
|
|
46
|
+
const target = event.target;
|
|
47
|
+
if (
|
|
48
|
+
target instanceof HTMLElement
|
|
49
|
+
&& target !== event.currentTarget
|
|
50
|
+
&& target.closest(INTERACTIVE_SELECTOR)
|
|
51
|
+
) return;
|
|
52
|
+
cancel();
|
|
53
|
+
startRef.current = { x: event.clientX, y: event.clientY };
|
|
54
|
+
timerRef.current = window.setTimeout(() => {
|
|
55
|
+
timerRef.current = null;
|
|
56
|
+
startRef.current = null;
|
|
57
|
+
suppressClickRef.current = true;
|
|
58
|
+
onSelectRef.current(id);
|
|
59
|
+
}, LONG_PRESS_MS);
|
|
60
|
+
},
|
|
61
|
+
onPointerMove: (event: ReactPointerEvent<HTMLElement>) => {
|
|
62
|
+
const start = startRef.current;
|
|
63
|
+
if (!start) return;
|
|
64
|
+
if (
|
|
65
|
+
Math.abs(event.clientX - start.x) > MOVE_TOLERANCE_PX
|
|
66
|
+
|| Math.abs(event.clientY - start.y) > MOVE_TOLERANCE_PX
|
|
67
|
+
) cancel();
|
|
68
|
+
},
|
|
69
|
+
onPointerUp: cancel,
|
|
70
|
+
onPointerCancel: cancel,
|
|
71
|
+
onClickCapture: (event: ReactMouseEvent<HTMLElement>) => {
|
|
72
|
+
if (!suppressClickRef.current) return false;
|
|
73
|
+
suppressClickRef.current = false;
|
|
74
|
+
event.preventDefault();
|
|
75
|
+
event.stopPropagation();
|
|
76
|
+
return true;
|
|
77
|
+
},
|
|
78
|
+
}), [cancel]);
|
|
79
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, type MouseEvent as ReactMouseEvent } from 'react';
|
|
4
|
+
import {
|
|
5
|
+
useCollectionInteractions,
|
|
6
|
+
type CollectionInteractionController,
|
|
7
|
+
type SelectionMarqueeRect,
|
|
8
|
+
} from './useCollectionInteractions';
|
|
9
|
+
|
|
10
|
+
const ROW_ATTR = 'data-notis-row-id';
|
|
11
|
+
|
|
12
|
+
export type DragRect = SelectionMarqueeRect;
|
|
13
|
+
|
|
14
|
+
export interface UseMultiSelectOptions<T> {
|
|
15
|
+
items: T[];
|
|
16
|
+
getId: (item: T) => string;
|
|
17
|
+
/** When false, suppresses the global Esc / Cmd+A / X / Shift+Arrow listeners. Default true. */
|
|
18
|
+
bindKeyboardShortcuts?: boolean;
|
|
19
|
+
/** When false, disables the rubber-band drag-select rectangle. Default true. */
|
|
20
|
+
enableDragSelect?: boolean;
|
|
21
|
+
/** Distance (px) the cursor must move past mousedown before a drag begins. Default 5. */
|
|
22
|
+
dragThreshold?: number;
|
|
23
|
+
/** Initial head id (the row that Shift+Arrow extends from). */
|
|
24
|
+
initialHeadId?: string | null;
|
|
25
|
+
/** Fires when the head moves via Shift+Arrow so the consumer can scroll the row into view. */
|
|
26
|
+
onHeadChange?: (id: string | null) => void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface MultiSelectController<T> {
|
|
30
|
+
selectedIds: ReadonlySet<string>;
|
|
31
|
+
selectedCount: number;
|
|
32
|
+
lastClickedId: string | null;
|
|
33
|
+
headId: string | null;
|
|
34
|
+
dragRect: DragRect | null;
|
|
35
|
+
isSelected: (id: string) => boolean;
|
|
36
|
+
getSelectedItems: () => T[];
|
|
37
|
+
toggle: (id: string) => void;
|
|
38
|
+
select: (ids: string[]) => void;
|
|
39
|
+
clear: () => void;
|
|
40
|
+
selectAll: () => void;
|
|
41
|
+
selectRange: (anchorId: string, targetId: string) => void;
|
|
42
|
+
setHead: (id: string | null) => void;
|
|
43
|
+
onCheckboxClick: (id: string) => (event: ReactMouseEvent) => void;
|
|
44
|
+
onRowMouseDown: (id: string) => (event: ReactMouseEvent) => void;
|
|
45
|
+
getRowProps: (id: string) => { [ROW_ATTR]: string };
|
|
46
|
+
getItemProps: CollectionInteractionController<T>['getItemProps'];
|
|
47
|
+
getCheckboxProps: (id: string) => {
|
|
48
|
+
isSelected: boolean;
|
|
49
|
+
onClick: (event: ReactMouseEvent) => void;
|
|
50
|
+
};
|
|
51
|
+
getContainerProps: CollectionInteractionController<T>['getContainerProps'];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @deprecated Use `useCollectionInteractions` for controlled selection,
|
|
56
|
+
* active-item navigation, scoped shortcuts, and semantic actions. This bridge
|
|
57
|
+
* is retained for the 0.2 minor and scheduled for removal in 0.3.
|
|
58
|
+
*/
|
|
59
|
+
export function useMultiSelect<T>(options: UseMultiSelectOptions<T>): MultiSelectController<T> {
|
|
60
|
+
const controller = useCollectionInteractions({
|
|
61
|
+
...options,
|
|
62
|
+
defaultActiveId: options.initialHeadId,
|
|
63
|
+
onActiveItemChange: options.onHeadChange,
|
|
64
|
+
shortcuts: {
|
|
65
|
+
next: false,
|
|
66
|
+
previous: false,
|
|
67
|
+
activate: false,
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const getRowProps = useCallback((id: string) => ({ [ROW_ATTR]: id }), []);
|
|
72
|
+
const getItemProps = controller.getItemProps;
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
selectedIds: controller.selectedIds,
|
|
76
|
+
selectedCount: controller.selectedCount,
|
|
77
|
+
lastClickedId: controller.lastClickedId,
|
|
78
|
+
headId: controller.headId,
|
|
79
|
+
dragRect: controller.dragRect,
|
|
80
|
+
isSelected: controller.isSelected,
|
|
81
|
+
getSelectedItems: controller.getSelectedItems,
|
|
82
|
+
toggle: controller.toggle,
|
|
83
|
+
select: controller.select,
|
|
84
|
+
clear: controller.clear,
|
|
85
|
+
selectAll: controller.selectAll,
|
|
86
|
+
selectRange: controller.selectRange,
|
|
87
|
+
setHead: controller.setActive,
|
|
88
|
+
onCheckboxClick: controller.onCheckboxClick,
|
|
89
|
+
onRowMouseDown: controller.onRowMouseDown,
|
|
90
|
+
getRowProps,
|
|
91
|
+
getItemProps,
|
|
92
|
+
getCheckboxProps: controller.getCheckboxProps,
|
|
93
|
+
getContainerProps: controller.getContainerProps,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { useNotisRuntime } from '../provider';
|
|
4
|
-
import type { AppDescriptor,
|
|
4
|
+
import type { AppDescriptor, CollectionItemDetail, DatabaseDescriptor, RouteDescriptor } from '../runtime';
|
|
5
5
|
|
|
6
6
|
interface NotisContext {
|
|
7
7
|
/** App metadata (id, name, icon, description). Null before runtime loads. */
|
|
@@ -10,14 +10,18 @@ interface NotisContext {
|
|
|
10
10
|
route: RouteDescriptor | null;
|
|
11
11
|
/** Databases declared by this app. Empty before runtime loads. */
|
|
12
12
|
databases: DatabaseDescriptor[];
|
|
13
|
+
/** Selected collection item for the current route, when applicable. */
|
|
14
|
+
collectionItem: CollectionItemDetail | null;
|
|
15
|
+
/** Resource requested through this route's canonical deep link. */
|
|
16
|
+
resourceId: string | null;
|
|
13
17
|
/** Whether the runtime is loaded and available. */
|
|
14
18
|
ready: boolean;
|
|
15
19
|
}
|
|
16
20
|
|
|
17
21
|
/**
|
|
18
|
-
* Access app-level metadata
|
|
19
|
-
*
|
|
20
|
-
*
|
|
22
|
+
* Access app-level metadata, the current route, declared databases, and the
|
|
23
|
+
* selected collection item. Returns safe defaults when the portal runtime is
|
|
24
|
+
* not available.
|
|
21
25
|
*/
|
|
22
26
|
export function useNotis(): NotisContext {
|
|
23
27
|
const runtime = useNotisRuntime();
|
|
@@ -26,6 +30,8 @@ export function useNotis(): NotisContext {
|
|
|
26
30
|
app: runtime?.app ?? null,
|
|
27
31
|
route: runtime?.route ?? null,
|
|
28
32
|
databases: runtime?.databases ?? [],
|
|
33
|
+
collectionItem: runtime?.context?.collectionItem ?? null,
|
|
34
|
+
resourceId: runtime?.context?.resourceId ?? null,
|
|
29
35
|
ready: runtime !== null,
|
|
30
36
|
};
|
|
31
37
|
}
|
|
@@ -5,9 +5,9 @@ import { useNotisRuntime } from '../provider';
|
|
|
5
5
|
|
|
6
6
|
interface NavigationActions {
|
|
7
7
|
/** Navigate to a route within the app by its path. */
|
|
8
|
-
toRoute: (path: string) => void;
|
|
8
|
+
toRoute: (path: string, options?: { resourceId?: string | null }) => void;
|
|
9
9
|
/** Navigate to a document detail view. */
|
|
10
|
-
toDocument: (documentId: string) => void;
|
|
10
|
+
toDocument: (documentId: string, title?: string | null) => void;
|
|
11
11
|
/** Navigate to the app's default route. */
|
|
12
12
|
toApp: () => void;
|
|
13
13
|
}
|
|
@@ -15,7 +15,7 @@ interface NavigationActions {
|
|
|
15
15
|
/**
|
|
16
16
|
* Navigation helpers for moving between routes and documents within the app.
|
|
17
17
|
* When rendered inside the portal, uses the runtime.navigate() bridge.
|
|
18
|
-
*
|
|
18
|
+
* Without a portal runtime, falls back to window.location for route navigation.
|
|
19
19
|
*
|
|
20
20
|
* ```tsx
|
|
21
21
|
* const nav = useNotisNavigation();
|
|
@@ -25,17 +25,20 @@ interface NavigationActions {
|
|
|
25
25
|
export function useNotisNavigation(): NavigationActions {
|
|
26
26
|
const runtime = useNotisRuntime();
|
|
27
27
|
|
|
28
|
-
const toRoute = useCallback((path: string) => {
|
|
28
|
+
const toRoute = useCallback((path: string, options?: { resourceId?: string | null }) => {
|
|
29
29
|
if (runtime?.navigate) {
|
|
30
|
-
runtime.navigate({ kind: 'route', path });
|
|
30
|
+
runtime.navigate({ kind: 'route', path, resourceId: options?.resourceId ?? null });
|
|
31
31
|
} else if (typeof window !== 'undefined') {
|
|
32
|
-
window.location.href
|
|
32
|
+
const url = new URL(path, window.location.href);
|
|
33
|
+
if (options?.resourceId) url.searchParams.set('resource', options.resourceId);
|
|
34
|
+
else url.searchParams.delete('resource');
|
|
35
|
+
window.location.href = url.toString();
|
|
33
36
|
}
|
|
34
37
|
}, [runtime]);
|
|
35
38
|
|
|
36
|
-
const toDocument = useCallback((documentId: string) => {
|
|
39
|
+
const toDocument = useCallback((documentId: string, title?: string | null) => {
|
|
37
40
|
if (runtime?.navigate) {
|
|
38
|
-
runtime.navigate({ kind: 'document', documentId });
|
|
41
|
+
runtime.navigate({ kind: 'document', documentId, title: title ?? undefined });
|
|
39
42
|
}
|
|
40
43
|
}, [runtime]);
|
|
41
44
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useSyncExternalStore } from 'react';
|
|
4
|
+
import { useNotisRuntime } from '../provider';
|
|
5
|
+
import { createQueryClient, queryKey } from '../queryCache';
|
|
6
|
+
|
|
7
|
+
export interface UseQueryOptions {
|
|
8
|
+
/** Required acknowledgement: only idempotent reads may run on mount or prefetch. */
|
|
9
|
+
readOnly: true;
|
|
10
|
+
enabled?: boolean;
|
|
11
|
+
staleTimeMs?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface UseQueryResult<T> {
|
|
15
|
+
data: T | undefined;
|
|
16
|
+
hasData: boolean;
|
|
17
|
+
loading: boolean;
|
|
18
|
+
isFetching: boolean;
|
|
19
|
+
error: Error | null;
|
|
20
|
+
refetch: () => Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Cached read callback; scope and lifetime are provided by the host, never by app globals. */
|
|
24
|
+
export function useQuery<T>(key: readonly unknown[], read: () => Promise<T>, options: UseQueryOptions): UseQueryResult<T> {
|
|
25
|
+
const runtime = useNotisRuntime();
|
|
26
|
+
// Older hosts deliberately get a hook-local cache: no cross-runtime/account reuse.
|
|
27
|
+
const fallback = useMemo(() => createQueryClient(), [runtime]);
|
|
28
|
+
const client = runtime?.queryClient ?? fallback;
|
|
29
|
+
const cacheKey = queryKey(key);
|
|
30
|
+
const enabled = Boolean(runtime) && options.enabled !== false && options.readOnly === true;
|
|
31
|
+
const readRef = useRef(read);
|
|
32
|
+
useLayoutEffect(() => { readRef.current = read; });
|
|
33
|
+
const subscribe = useCallback((listener: () => void) => enabled ? client.subscribe(cacheKey, listener) : () => {}, [cacheKey, client, enabled]);
|
|
34
|
+
const snapshot = useSyncExternalStore(subscribe, () => client.getSnapshot<T>(cacheKey), () => client.getSnapshot<T>(cacheKey));
|
|
35
|
+
const fetch = useCallback(() => {
|
|
36
|
+
const capturedRead = readRef.current;
|
|
37
|
+
return client.fetch(cacheKey, capturedRead, { staleTimeMs: options.staleTimeMs });
|
|
38
|
+
}, [cacheKey, client, options.staleTimeMs]);
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (!enabled) return;
|
|
41
|
+
void fetch().catch(() => {}); // Error belongs to the snapshot; no unhandled rejection.
|
|
42
|
+
}, [enabled, fetch, snapshot.invalidation]);
|
|
43
|
+
const refetch = useCallback(async () => {
|
|
44
|
+
if (!enabled) return;
|
|
45
|
+
client.invalidate(cacheKey);
|
|
46
|
+
await fetch().catch(() => {});
|
|
47
|
+
}, [cacheKey, client, enabled, fetch]);
|
|
48
|
+
return {
|
|
49
|
+
data: enabled ? snapshot.data : undefined,
|
|
50
|
+
hasData: enabled && snapshot.hasData,
|
|
51
|
+
loading: enabled && !snapshot.hasData && !snapshot.error,
|
|
52
|
+
isFetching: enabled && snapshot.isFetching,
|
|
53
|
+
error: enabled ? snapshot.error : null,
|
|
54
|
+
refetch,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Opt-in preparation for small, known read queries. Never use for writes or full-database crawls. */
|
|
59
|
+
export function useQueryClient(): {
|
|
60
|
+
invalidate(key?: readonly unknown[]): void;
|
|
61
|
+
prefetch<T>(key: readonly unknown[], read: () => Promise<T>, options: UseQueryOptions): Promise<T | undefined>;
|
|
62
|
+
} {
|
|
63
|
+
const runtime = useNotisRuntime();
|
|
64
|
+
return useMemo(() => ({
|
|
65
|
+
invalidate: (key?: readonly unknown[]) => runtime?.queryClient?.invalidate(key ? queryKey(key) : undefined),
|
|
66
|
+
prefetch: <T,>(key: readonly unknown[], read: () => Promise<T>, options: UseQueryOptions) => {
|
|
67
|
+
if (!runtime?.queryClient || options.enabled === false || options.readOnly !== true) return Promise.resolve(undefined);
|
|
68
|
+
return runtime.queryClient.prefetch(queryKey(key), read, { staleTimeMs: options.staleTimeMs });
|
|
69
|
+
},
|
|
70
|
+
}), [runtime]);
|
|
71
|
+
}
|