@notis_ai/cli 0.2.0-beta.19.1 → 0.2.0-beta.191.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 +421 -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 +19299 -0
- package/dist/base-skills/notis-apps/SKILL.md +72 -0
- package/dist/base-skills/notis-apps/references/architecture.md +167 -0
- package/dist/base-skills/notis-apps/references/context.md +81 -0
- package/dist/base-skills/notis-apps/references/design.md +178 -0
- package/dist/base-skills/notis-apps/references/reading.md +89 -0
- package/dist/base-skills/notis-apps/references/release.md +145 -0
- package/dist/base-skills/notis-apps/references/sdk.md +63 -0
- package/dist/base-skills/notis-apps/references/troubleshooting.md +23 -0
- package/dist/base-skills/notis-cli/SKILL.md +154 -0
- package/dist/base-skills/notis-cli/references/app-delivery.md +18 -0
- package/dist/base-skills/notis-cli/references/intelligence.md +94 -0
- package/dist/base-skills/notis-cli/references/native-databases.md +21 -0
- package/dist/base-skills/notis-cli/references/tool-examples.md +56 -0
- package/dist/base-skills/notis-cli/references/troubleshooting.md +39 -0
- package/dist/base-skills/notis-query/SKILL.md +67 -0
- package/dist/base-skills/notis-query/references/database-discovery.md +70 -0
- package/dist/base-skills/notis-query/references/documents.md +50 -0
- package/dist/base-skills/notis-query/references/query.md +543 -0
- package/{template/packages/notis-sdk → dist/sdk}/package.json +14 -4
- package/dist/sdk/src/agentContext.ts +36 -0
- package/dist/sdk/src/components/Dialog.tsx +46 -0
- package/dist/sdk/src/components/DocumentEditor.tsx +103 -0
- package/dist/sdk/src/components/Markdown.tsx +60 -0
- package/dist/sdk/src/components/MarkdownEditor.tsx +121 -0
- package/dist/sdk/src/components/MultiSelectActionBar.tsx +285 -0
- package/dist/sdk/src/components/MultiSelectCheckbox.tsx +97 -0
- package/dist/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
- package/dist/sdk/src/components/NotisCommentBoundary.tsx +172 -0
- package/dist/sdk/src/components/NotisSelectionBoundary.tsx +59 -0
- package/dist/sdk/src/components/ShortcutHints.tsx +56 -0
- package/dist/sdk/src/components/Skeleton.tsx +24 -0
- package/dist/sdk/src/config.ts +261 -0
- package/dist/sdk/src/documents.ts +256 -0
- package/dist/sdk/src/hooks/useActiveResource.ts +19 -0
- package/dist/sdk/src/hooks/useAgentContext.ts +23 -0
- package/dist/sdk/src/hooks/useCloudComputer.ts +64 -0
- package/dist/sdk/src/hooks/useCollectionInteractions.ts +838 -0
- package/dist/sdk/src/hooks/useDatabaseSchema.ts +49 -0
- package/dist/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
- package/dist/sdk/src/hooks/useDocument.ts +43 -0
- package/dist/sdk/src/hooks/useDocuments.ts +84 -0
- package/dist/sdk/src/hooks/useHandover.ts +77 -0
- package/dist/sdk/src/hooks/useLongPressSelection.ts +79 -0
- package/dist/sdk/src/hooks/useMultiSelect.ts +95 -0
- package/{template/packages/notis-sdk → dist/sdk}/src/hooks/useNotis.ts +12 -4
- package/{template/packages/notis-sdk → dist/sdk}/src/hooks/useNotisNavigation.ts +11 -8
- package/dist/sdk/src/hooks/useQuery.ts +71 -0
- package/dist/sdk/src/hooks/useTool.ts +65 -0
- package/dist/sdk/src/hooks/useToolQuery.ts +12 -0
- package/dist/sdk/src/hooks/useTopBarSearch.ts +81 -0
- package/dist/sdk/src/hooks/useUpsertDocument.ts +95 -0
- package/dist/sdk/src/index.ts +164 -0
- package/dist/sdk/src/interactions/actions.ts +59 -0
- package/dist/sdk/src/interactions/shortcuts.tsx +726 -0
- package/dist/sdk/src/interactions/visibility.ts +13 -0
- package/dist/sdk/src/interactions.ts +45 -0
- package/dist/sdk/src/provider.tsx +44 -0
- package/dist/sdk/src/queryCache.ts +170 -0
- package/dist/sdk/src/runtime.ts +465 -0
- package/dist/sdk/src/styles.css +266 -0
- package/dist/sdk/src/tailwind.ts +66 -0
- package/dist/sdk/src/vite.ts +73 -0
- package/dist/skill-sync/index.js +1753 -0
- package/dist/skill-sync/index.js.map +7 -0
- package/dist/skill-sync-worker.mjs +3113 -0
- package/package.json +18 -7
- package/skills/notis-apps/cli.md +299 -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 +1483 -203
- package/src/command-specs/auth.js +114 -137
- package/src/command-specs/diagnostics.js +730 -0
- package/src/command-specs/handover.js +374 -0
- package/src/command-specs/helpers.js +85 -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 +97 -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 +677 -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 +2652 -205
- package/src/runtime/app-registry-scaffolds.js +367 -0
- package/src/runtime/app-test-server.js +293 -0
- package/src/runtime/assets/store-screenshot-dark.png +0 -0
- package/src/runtime/auth-recovery.js +110 -0
- package/src/runtime/base-skills.d.ts +20 -0
- package/src/runtime/base-skills.js +167 -0
- package/src/runtime/channel.js +133 -0
- package/src/runtime/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/screenshot-framing.js +85 -0
- package/src/runtime/skill-sync/cloud-client.ts +99 -0
- package/src/runtime/skill-sync/index.ts +786 -0
- package/src/runtime/skill-sync/local-scanner.ts +1151 -0
- package/src/runtime/skill-sync/symlink-manager.ts +433 -0
- package/src/runtime/skill-sync/sync-plan.ts +47 -0
- package/src/runtime/skill-sync/types.ts +131 -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 +146 -0
- package/src/runtime/sync-skills.d.ts +37 -0
- package/src/runtime/sync-skills.js +231 -0
- package/src/runtime/telemetry.js +92 -0
- package/src/runtime/transport.js +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 +435 -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/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/tsconfig.json +0 -15
- /package/{template/packages/notis-sdk → dist/sdk}/src/hooks/useBackend.ts +0 -0
- /package/{template/packages/notis-sdk → dist/sdk}/src/hooks/useTools.ts +0 -0
- /package/{template/packages/notis-sdk → dist/sdk}/src/ui.ts +0 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { type CSSProperties, type ReactElement } from 'react';
|
|
4
|
+
import type { DragRect } from '../hooks/useMultiSelect';
|
|
5
|
+
|
|
6
|
+
export interface MultiSelectDragOverlayProps {
|
|
7
|
+
rect: DragRect | null;
|
|
8
|
+
className?: string;
|
|
9
|
+
style?: CSSProperties;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const overlayBaseStyle: CSSProperties = {
|
|
13
|
+
position: 'fixed',
|
|
14
|
+
borderRadius: '3px',
|
|
15
|
+
border: '1px solid rgba(35, 131, 226, 0.3)',
|
|
16
|
+
background: 'rgba(35, 131, 226, 0.08)',
|
|
17
|
+
pointerEvents: 'none',
|
|
18
|
+
zIndex: 50,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export function MultiSelectDragOverlay({
|
|
22
|
+
rect,
|
|
23
|
+
className,
|
|
24
|
+
style,
|
|
25
|
+
}: MultiSelectDragOverlayProps): ReactElement | null {
|
|
26
|
+
if (!rect) return null;
|
|
27
|
+
if (rect.width === 0 && rect.height === 0) return null;
|
|
28
|
+
|
|
29
|
+
const merged: CSSProperties = {
|
|
30
|
+
...overlayBaseStyle,
|
|
31
|
+
left: rect.left,
|
|
32
|
+
top: rect.top,
|
|
33
|
+
width: rect.width,
|
|
34
|
+
height: rect.height,
|
|
35
|
+
...(style || null),
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
return <div aria-hidden className={className} style={merged} />;
|
|
39
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React, { useEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react';
|
|
3
|
+
import type { ContextResource } from '../runtime';
|
|
4
|
+
import { NotisSelectionBoundary } from './NotisSelectionBoundary';
|
|
5
|
+
import { useAgentContext } from '../hooks/useAgentContext';
|
|
6
|
+
|
|
7
|
+
export interface NotisCommentBoxProps {
|
|
8
|
+
quote?: string;
|
|
9
|
+
value: string;
|
|
10
|
+
onChange(value: string): void;
|
|
11
|
+
onSubmit(): void;
|
|
12
|
+
onCancel(): void;
|
|
13
|
+
pending?: boolean;
|
|
14
|
+
error?: string;
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Compact, chrome-themed presentation. Apps retain ownership of comment state. */
|
|
19
|
+
export function NotisCommentBox({ quote, value, onChange, onSubmit, onCancel, pending, error, className = '' }: NotisCommentBoxProps) {
|
|
20
|
+
return <div data-notis-comment-ui role="dialog" aria-label="Comment on selection"
|
|
21
|
+
style={{ colorScheme: 'dark', background: 'hsl(var(--sidebar-background, 0 0% 7.5%))', color: 'hsl(var(--sidebar-foreground, 240 4.8% 95.9%))', borderColor: 'hsl(var(--sidebar-border, 0 0% 23.5%))' }}
|
|
22
|
+
className={`flex min-w-0 flex-col gap-2 rounded-xl border p-2.5 text-sm shadow-lg ${className}`}>
|
|
23
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
24
|
+
{quote && <blockquote title={quote} className="min-w-0 flex-1 truncate border-l-2 border-current/40 pl-2 opacity-70">{quote}</blockquote>}
|
|
25
|
+
<button type="button" aria-label="Cancel comment" onClick={onCancel} disabled={pending} className="ml-auto flex size-7 shrink-0 items-center justify-center rounded-lg opacity-70 hover:bg-white/10 hover:opacity-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current disabled:opacity-40 [@media(pointer:coarse)]:size-9">
|
|
26
|
+
<svg aria-hidden="true" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"><path d="m6 6 12 12M18 6 6 18" /></svg>
|
|
27
|
+
</button>
|
|
28
|
+
</div>
|
|
29
|
+
<textarea autoFocus rows={2} name="selection-comment" aria-label="Your comment" placeholder="Add a comment…" value={value} onChange={event => onChange(event.target.value)} disabled={pending}
|
|
30
|
+
onKeyDown={event => { if (event.nativeEvent.isComposing || pending) return; if (event.key === 'Escape') { event.preventDefault(); onCancel(); } if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) { event.preventDefault(); if (value.trim()) onSubmit(); } }}
|
|
31
|
+
className="w-full min-w-0 resize-none rounded bg-transparent px-0.5 py-1 text-base text-inherit outline-none placeholder:text-current placeholder:opacity-60 sm:text-sm" />
|
|
32
|
+
{error && <p role="alert" className="text-sm text-red-300">{error}</p>}
|
|
33
|
+
<div className="flex items-center justify-between gap-3">
|
|
34
|
+
<span className="text-xs opacity-60">⌘ / Ctrl ↵ to add</span>
|
|
35
|
+
<button type="button" disabled={pending || !value.trim()} onClick={onSubmit}
|
|
36
|
+
style={{ background: 'hsl(var(--sidebar-foreground, 240 4.8% 95.9%))', color: 'hsl(var(--sidebar-background, 0 0% 7.5%))' }}
|
|
37
|
+
className="min-h-8 shrink-0 rounded-lg px-2.5 text-sm font-medium hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current disabled:opacity-40 [@media(pointer:coarse)]:min-h-9">{pending ? 'Adding…' : 'Add to chat'}</button>
|
|
38
|
+
</div>
|
|
39
|
+
</div>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface NotisCommentBoundaryProps {
|
|
43
|
+
resource?: ContextResource | null;
|
|
44
|
+
children: ReactNode;
|
|
45
|
+
className?: string;
|
|
46
|
+
commentClassName?: string;
|
|
47
|
+
/** Replace the optional standard editor while retaining selection/context behavior. */
|
|
48
|
+
renderComment?: (props: NotisCommentBoxProps) => ReactNode;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type SelectionRect = Pick<DOMRect, 'left' | 'right' | 'top' | 'bottom'>;
|
|
52
|
+
|
|
53
|
+
/** Place the tile after the final selected line, otherwise below (or above near the viewport bottom). */
|
|
54
|
+
export function getCommentActionPosition(rects: SelectionRect[], viewport: { width: number; height: number }, rightBoundary = viewport.width, trailing: SelectionRect[] = []) {
|
|
55
|
+
const last = rects[rects.length - 1];
|
|
56
|
+
const size = 32, gap = 8;
|
|
57
|
+
const clampX = (x: number) => Math.max(gap, Math.min(viewport.width - size - gap, x));
|
|
58
|
+
const clampY = (y: number) => Math.max(gap, Math.min(viewport.height - size - gap, y));
|
|
59
|
+
const beside = { left: last.right + gap, top: clampY((last.top + last.bottom - size) / 2) };
|
|
60
|
+
const overlapsText = trailing.some(rect => rect.left < beside.left + size && rect.right > beside.left && rect.top < beside.top + size && rect.bottom > beside.top);
|
|
61
|
+
if (beside.left + size <= Math.min(viewport.width, rightBoundary) - gap && !overlapsText) return beside;
|
|
62
|
+
return { left: clampX(last.right - size), top: clampY(last.bottom + gap + size <= viewport.height - gap ? last.bottom + gap : last.top - size - gap) };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Browser paragraph selection may include an adjacent block's empty starting boundary. */
|
|
66
|
+
export function getOwnedCommentRange(selected: Range, content: Element): Range | null {
|
|
67
|
+
const owned = content.ownerDocument.createRange();
|
|
68
|
+
owned.selectNodeContents(content);
|
|
69
|
+
const range = selected.cloneRange();
|
|
70
|
+
// Only clip whitespace outside the resource; actual neighboring content must
|
|
71
|
+
// never be silently attached to this resource's comment.
|
|
72
|
+
if (range.compareBoundaryPoints(Range.START_TO_START, owned) < 0) {
|
|
73
|
+
const before = range.cloneRange();
|
|
74
|
+
before.setEnd(owned.startContainer, owned.startOffset);
|
|
75
|
+
if (before.toString().trim()) return null;
|
|
76
|
+
range.setStart(owned.startContainer, owned.startOffset);
|
|
77
|
+
}
|
|
78
|
+
if (range.compareBoundaryPoints(Range.END_TO_END, owned) > 0) {
|
|
79
|
+
const after = range.cloneRange();
|
|
80
|
+
after.setStart(owned.endContainer, owned.endOffset);
|
|
81
|
+
if (after.toString().trim()) return null;
|
|
82
|
+
range.setEnd(owned.endContainer, owned.endOffset);
|
|
83
|
+
}
|
|
84
|
+
if (range.collapsed || !content.contains(range.startContainer) || !content.contains(range.endContainer)) return null;
|
|
85
|
+
return range;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Selection-to-chat convenience. No annotation database, polling or background attachment. */
|
|
89
|
+
export function NotisCommentBoundary({ resource, children, className, commentClassName, renderComment }: NotisCommentBoundaryProps) {
|
|
90
|
+
const context = useAgentContext();
|
|
91
|
+
const root = useRef<HTMLDivElement>(null);
|
|
92
|
+
const [selection, setSelection] = useState<{ id: string; text: string; resource?: ContextResource | null; top: number; left: number } | null>(null);
|
|
93
|
+
const [editing, setEditing] = useState(false);
|
|
94
|
+
const editingRef = useRef(false);
|
|
95
|
+
const [comment, setComment] = useState('');
|
|
96
|
+
const [pending, setPending] = useState(false);
|
|
97
|
+
const [error, setError] = useState('');
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
const node = root.current;
|
|
100
|
+
if (!node) return;
|
|
101
|
+
const document = node.ownerDocument;
|
|
102
|
+
const scope = node.getRootNode() as ShadowRoot & { getSelection?: () => Selection | null };
|
|
103
|
+
const clearInvalidSelection = () => {
|
|
104
|
+
// Opening the editor focuses its input and collapses the browser range.
|
|
105
|
+
// Keep the captured quote/editor; only the unopened action follows live selection.
|
|
106
|
+
if (editingRef.current) return;
|
|
107
|
+
const selected = scope.getSelection?.() || document.defaultView?.getSelection();
|
|
108
|
+
const content = node.firstElementChild;
|
|
109
|
+
if (!selected?.rangeCount || selected.isCollapsed || !selected.toString().trim()
|
|
110
|
+
|| !content || !getOwnedCommentRange(selected.getRangeAt(0), content)) setSelection(null);
|
|
111
|
+
};
|
|
112
|
+
document.addEventListener('selectionchange', clearInvalidSelection);
|
|
113
|
+
if (scope !== (document as unknown as ShadowRoot)) scope.addEventListener('selectionchange', clearInvalidSelection);
|
|
114
|
+
return () => {
|
|
115
|
+
document.removeEventListener('selectionchange', clearInvalidSelection);
|
|
116
|
+
if (scope !== (document as unknown as ShadowRoot)) scope.removeEventListener('selectionchange', clearInvalidSelection);
|
|
117
|
+
};
|
|
118
|
+
}, []);
|
|
119
|
+
const open = () => { editingRef.current = true; setEditing(true); };
|
|
120
|
+
const capture = (target: EventTarget | null) => {
|
|
121
|
+
// Clicking the action/editor must not unmount it before its click runs.
|
|
122
|
+
if (editing || (target instanceof Element && target.closest('[data-notis-comment-ui]'))) return;
|
|
123
|
+
const node = root.current;
|
|
124
|
+
const scope = node?.getRootNode() as ShadowRoot & { getSelection?: () => Selection | null };
|
|
125
|
+
const selected = scope?.getSelection?.() || window.getSelection();
|
|
126
|
+
if (!node || !selected?.rangeCount || selected.isCollapsed) { setSelection(null); return; }
|
|
127
|
+
const content = node.firstElementChild;
|
|
128
|
+
const range = content ? getOwnedCommentRange(selected.getRangeAt(0), content) : null;
|
|
129
|
+
if (!range) { setSelection(null); return; }
|
|
130
|
+
const start = range.startContainer.nodeType === Node.ELEMENT_NODE ? range.startContainer as Element : range.startContainer.parentElement;
|
|
131
|
+
const end = range.endContainer.nodeType === Node.ELEMENT_NODE ? range.endContainer as Element : range.endContainer.parentElement;
|
|
132
|
+
if (!node.contains(range.startContainer) || !node.contains(range.endContainer)
|
|
133
|
+
|| start?.closest('input,textarea,button,[contenteditable],[data-notis-comment-ui]')
|
|
134
|
+
|| end?.closest('input,textarea,button,[contenteditable],[data-notis-comment-ui]')) { setSelection(null); return; }
|
|
135
|
+
const rect = range.getBoundingClientRect();
|
|
136
|
+
const lines = Array.from(range.getClientRects?.() || []).filter(line => line.width > 0 && line.height > 0);
|
|
137
|
+
const boundary = node.firstElementChild?.getBoundingClientRect();
|
|
138
|
+
const trailing: DOMRect[] = [];
|
|
139
|
+
const endBlock = end?.closest('p,li,h1,h2,h3,h4,h5,h6,td,th,div');
|
|
140
|
+
if (endBlock && node.contains(endBlock)) {
|
|
141
|
+
const remainder = range.cloneRange();
|
|
142
|
+
remainder.selectNodeContents(endBlock);
|
|
143
|
+
remainder.setStart(range.endContainer, range.endOffset);
|
|
144
|
+
trailing.push(...Array.from(remainder.getClientRects?.() || []).filter(line => line.width > 0 && line.height > 0));
|
|
145
|
+
}
|
|
146
|
+
const position = getCommentActionPosition(lines.length ? lines : [rect], { width: window.innerWidth, height: window.innerHeight }, boundary?.width ? boundary.right : window.innerWidth, trailing);
|
|
147
|
+
setSelection({ id: crypto.randomUUID(), text: selected.toString().trim(), resource: resource ? JSON.parse(JSON.stringify(resource)) : null, ...position });
|
|
148
|
+
};
|
|
149
|
+
const close = () => { editingRef.current = false; setEditing(false); setSelection(null); setComment(''); setError(''); };
|
|
150
|
+
const submit = async () => {
|
|
151
|
+
if (!selection || !comment.trim() || pending) return;
|
|
152
|
+
setPending(true); setError('');
|
|
153
|
+
try {
|
|
154
|
+
const added = await context.add({ id: selection.id, kind: 'comment', title: selection.resource?.label || 'Comment', icon: 'phosphor:chat-text', text: selection.text, comment, resource: selection.resource });
|
|
155
|
+
if (!added) throw new Error('The comment could not be added to chat.');
|
|
156
|
+
close();
|
|
157
|
+
} catch (reason) { setError(reason instanceof Error ? reason.message : 'Could not add this comment.'); }
|
|
158
|
+
finally { setPending(false); }
|
|
159
|
+
};
|
|
160
|
+
const editorProps: NotisCommentBoxProps = { quote: selection?.text, value: comment, onChange: setComment, onSubmit: () => void submit(), onCancel: close, pending, error, className: commentClassName };
|
|
161
|
+
return <div ref={root} className="contents" onMouseUp={event => capture(event.target)} onDoubleClick={event => capture(event.target)} onKeyUp={event => { if (!editing && (event.shiftKey || event.key === 'Shift' || ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'a'))) capture(event.target); }}>
|
|
162
|
+
<NotisSelectionBoundary resource={resource} className={className} style={className ? undefined : { display: 'contents' }}>{children}</NotisSelectionBoundary>
|
|
163
|
+
{selection && <div data-notis-comment-ui className="fixed z-50 max-h-[calc(100dvh-1rem)]" style={{ overflow: editing ? 'auto' : 'visible', top: editing ? Math.min(selection.top, Math.max(8, window.innerHeight - 320)) : selection.top, left: `clamp(8px, ${selection.left}px, calc(100vw - ${editing ? 'min(20rem, calc(100vw - 1rem))' : '2rem'} - 8px))`, width: editing ? 'min(20rem, calc(100vw - 1rem))' : undefined } as CSSProperties}>
|
|
164
|
+
{editing ? (renderComment ? renderComment(editorProps) : <NotisCommentBox {...editorProps} />) : <button type="button" aria-label="Comment" title="Comment" onMouseDown={event => event.preventDefault()} onClick={open}
|
|
165
|
+
style={{ background: 'hsl(var(--sidebar-background, 0 0% 7.5%))', color: 'hsl(var(--sidebar-foreground, 240 4.8% 95.9%))' }}
|
|
166
|
+
className="relative flex size-8 items-center justify-center rounded-lg ring-1 ring-white/15 hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current">
|
|
167
|
+
<svg aria-hidden="true" width="18" height="18" viewBox="0 0 256 256" fill="currentColor"><path d="M216,48H40A16,16,0,0,0,24,64V224a15.85,15.85,0,0,0,9.24,14.5A16.13,16.13,0,0,0,40,240a15.89,15.89,0,0,0,10.25-3.78l.09-.07L83,208H216a16,16,0,0,0,16-16V64A16,16,0,0,0,216,48ZM40,224h0ZM216,192H80a8,8,0,0,0-5.23,1.95L40,224V64H216ZM88,112a8,8,0,0,1,8-8h64a8,8,0,0,1,0,16H96A8,8,0,0,1,88,112Zm0,32a8,8,0,0,1,8-8h64a8,8,0,1,1,0,16H96A8,8,0,0,1,88,144Z" /></svg>
|
|
168
|
+
<span aria-hidden="true" className="absolute left-1/2 top-1/2 hidden size-12 -translate-x-1/2 -translate-y-1/2 [@media(pointer:coarse)]:block" />
|
|
169
|
+
</button>}
|
|
170
|
+
</div>}
|
|
171
|
+
</div>;
|
|
172
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { CSSProperties, ClipboardEvent, ReactNode } from 'react';
|
|
2
|
+
import React, { useCallback } from 'react';
|
|
3
|
+
import { useNotisRuntime } from '../provider';
|
|
4
|
+
import type { ContextResource, ContextSelection } from '../runtime';
|
|
5
|
+
|
|
6
|
+
export const NOTIS_CONTEXT_CLIPBOARD_TYPE = 'application/x-notis-context+json';
|
|
7
|
+
|
|
8
|
+
export interface NotisSelectionBoundaryProps {
|
|
9
|
+
resource?: ContextResource | null;
|
|
10
|
+
children: ReactNode;
|
|
11
|
+
className?: string;
|
|
12
|
+
style?: CSSProperties;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function selectionId(): string {
|
|
16
|
+
return typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
|
17
|
+
? crypto.randomUUID()
|
|
18
|
+
: `selection-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Adds structured provenance to ordinary copied text. Pasting still works in
|
|
23
|
+
* every app because `text/plain` is preserved; Notis chat additionally reads
|
|
24
|
+
* the custom MIME payload and renders a separate quote pill.
|
|
25
|
+
*/
|
|
26
|
+
export function NotisSelectionBoundary({
|
|
27
|
+
resource,
|
|
28
|
+
children,
|
|
29
|
+
className,
|
|
30
|
+
style,
|
|
31
|
+
}: NotisSelectionBoundaryProps) {
|
|
32
|
+
const runtime = useNotisRuntime();
|
|
33
|
+
const onCopy = useCallback((event: ClipboardEvent<HTMLDivElement>) => {
|
|
34
|
+
const target = event.target as Element | null;
|
|
35
|
+
if (target?.closest?.('input,textarea,[contenteditable="true"]')) return;
|
|
36
|
+
const scope = event.currentTarget.getRootNode() as ShadowRoot & { getSelection?: () => Selection | null };
|
|
37
|
+
const selection = scope?.getSelection?.() || window.getSelection();
|
|
38
|
+
const text = selection?.toString() ?? '';
|
|
39
|
+
if (
|
|
40
|
+
!text.trim()
|
|
41
|
+
|| !selection
|
|
42
|
+
|| !event.currentTarget.contains(selection.anchorNode)
|
|
43
|
+
|| !event.currentTarget.contains(selection.focusNode)
|
|
44
|
+
) return;
|
|
45
|
+
|
|
46
|
+
const payload: ContextSelection = {
|
|
47
|
+
id: selectionId(),
|
|
48
|
+
text,
|
|
49
|
+
...(runtime?.contextSource ? { source: runtime.contextSource } : {}),
|
|
50
|
+
...(resource ? { resource: { ...resource, snapshot: undefined } } : {}),
|
|
51
|
+
};
|
|
52
|
+
event.clipboardData.setData('text/plain', text);
|
|
53
|
+
event.clipboardData.setData(NOTIS_CONTEXT_CLIPBOARD_TYPE, JSON.stringify(payload));
|
|
54
|
+
event.preventDefault();
|
|
55
|
+
runtime?.captureContextSelection?.(payload);
|
|
56
|
+
}, [resource, runtime]);
|
|
57
|
+
|
|
58
|
+
return <div className={className} style={style} onCopy={onCopy}>{children}</div>;
|
|
59
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import React, { type CSSProperties, type ReactElement } from 'react';
|
|
4
|
+
import { shortcutDisplay } from '../interactions/shortcuts';
|
|
5
|
+
|
|
6
|
+
export interface ShortcutHint {
|
|
7
|
+
id: string;
|
|
8
|
+
keys: string;
|
|
9
|
+
label: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ShortcutHintsProps {
|
|
13
|
+
shortcuts: ShortcutHint[];
|
|
14
|
+
className?: string;
|
|
15
|
+
style?: CSSProperties;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const listStyle: CSSProperties = {
|
|
19
|
+
display: 'flex',
|
|
20
|
+
flexWrap: 'wrap',
|
|
21
|
+
alignItems: 'center',
|
|
22
|
+
gap: '0.75rem',
|
|
23
|
+
color: 'hsl(var(--muted-foreground))',
|
|
24
|
+
fontSize: '12px',
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const hintStyle: CSSProperties = {
|
|
28
|
+
display: 'inline-flex',
|
|
29
|
+
alignItems: 'center',
|
|
30
|
+
gap: '0.35rem',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const keyStyle: CSSProperties = {
|
|
34
|
+
minWidth: '20px',
|
|
35
|
+
border: '1px solid hsl(var(--border))',
|
|
36
|
+
borderRadius: '4px',
|
|
37
|
+
background: 'hsl(var(--muted))',
|
|
38
|
+
color: 'hsl(var(--foreground))',
|
|
39
|
+
padding: '1px 5px',
|
|
40
|
+
textAlign: 'center',
|
|
41
|
+
font: 'inherit',
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export function ShortcutHints({ shortcuts, className, style }: ShortcutHintsProps): ReactElement | null {
|
|
45
|
+
if (shortcuts.length === 0) return null;
|
|
46
|
+
return (
|
|
47
|
+
<div aria-label="Keyboard shortcuts" className={className} style={{ ...listStyle, ...style }}>
|
|
48
|
+
{shortcuts.map((shortcut) => (
|
|
49
|
+
<span key={shortcut.id} style={hintStyle}>
|
|
50
|
+
<kbd style={keyStyle}>{shortcutDisplay(shortcut.keys)}</kbd>
|
|
51
|
+
<span>{shortcut.label}</span>
|
|
52
|
+
</span>
|
|
53
|
+
))}
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { CSSProperties } from 'react';
|
|
2
|
+
|
|
3
|
+
/** Content-shaped placeholder. Never use it to replace a successful cached result. */
|
|
4
|
+
export function Skeleton({ className, style }: { className?: string; style?: CSSProperties }) {
|
|
5
|
+
return <span aria-hidden="true" className={className} style={{ display: 'block', height: 16,
|
|
6
|
+
// Apps may define --muted as HSL channels or a complete CSS color. A
|
|
7
|
+
// neutral default remains visible with either token format and no theme.
|
|
8
|
+
borderRadius: 6, background: 'rgba(128,128,128,.18)', ...style }} />;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function ViewSkeleton({ variant = 'table', rows = 5 }: {
|
|
12
|
+
variant?: 'table' | 'cards' | 'graph' | 'detail'; rows?: number;
|
|
13
|
+
}) {
|
|
14
|
+
return <div role="status" aria-label="Loading content" aria-busy="true" data-notis-content-skeleton
|
|
15
|
+
style={{ width: '100%', display: 'grid', gap: 16, padding: 24 }}>
|
|
16
|
+
<Skeleton style={{ width: '32%', height: 28 }} />
|
|
17
|
+
{variant === 'graph' ? <Skeleton style={{ height: 420 }} /> :
|
|
18
|
+
<div style={{ display: 'grid', gap: 12, gridTemplateColumns: variant === 'cards' ? 'repeat(auto-fit,minmax(180px,1fr))' : '1fr' }}>
|
|
19
|
+
{Array.from({ length: rows }, (_, index) => <Skeleton key={index}
|
|
20
|
+
style={{ height: variant === 'cards' ? 112 : variant === 'detail' ? 20 : 44,
|
|
21
|
+
width: variant === 'detail' && index === rows - 1 ? '65%' : '100%' }} />)}
|
|
22
|
+
</div>}
|
|
23
|
+
</div>;
|
|
24
|
+
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration utilities for notis.config.ts.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* ```ts
|
|
6
|
+
* // notis.config.ts
|
|
7
|
+
* import { defineNotisApp } from '@notis/sdk/config';
|
|
8
|
+
*
|
|
9
|
+
* export default defineNotisApp({
|
|
10
|
+
* name: 'My App',
|
|
11
|
+
* description: 'Does things',
|
|
12
|
+
* icon: 'phosphor:squares-four',
|
|
13
|
+
* databases: ['tasks'],
|
|
14
|
+
* routes: [
|
|
15
|
+
* {
|
|
16
|
+
* path: '/',
|
|
17
|
+
* slug: 'notes',
|
|
18
|
+
* name: 'Notes',
|
|
19
|
+
* default: true,
|
|
20
|
+
* collection: {
|
|
21
|
+
* database: 'notes',
|
|
22
|
+
* titleProperty: 'Name',
|
|
23
|
+
* parentProperty: 'Parent',
|
|
24
|
+
* sidebar: {
|
|
25
|
+
* mode: 'tree',
|
|
26
|
+
* allowCreate: true,
|
|
27
|
+
* },
|
|
28
|
+
* },
|
|
29
|
+
* },
|
|
30
|
+
* ],
|
|
31
|
+
* tools: [...],
|
|
32
|
+
* });
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
export interface NotisRouteConfig {
|
|
37
|
+
path: string;
|
|
38
|
+
slug: string;
|
|
39
|
+
name: string;
|
|
40
|
+
icon?: string;
|
|
41
|
+
parentSlug?: string | null;
|
|
42
|
+
default?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Allow the host to address an app-owned resource on this route through the
|
|
45
|
+
* canonical `?resource=<id>` deep link. Read the incoming id with
|
|
46
|
+
* `useNotis().resourceId`.
|
|
47
|
+
*/
|
|
48
|
+
resourceDeepLinks?: boolean;
|
|
49
|
+
exportName?: string;
|
|
50
|
+
collection?: {
|
|
51
|
+
database: string;
|
|
52
|
+
titleProperty: string;
|
|
53
|
+
parentProperty?: string | null;
|
|
54
|
+
sidebar?: {
|
|
55
|
+
mode: 'flat-list' | 'tree';
|
|
56
|
+
allowCreate: boolean;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export const NOTIS_APP_CATEGORIES = [
|
|
62
|
+
'Productivity',
|
|
63
|
+
'Sales & Marketing',
|
|
64
|
+
'Operations',
|
|
65
|
+
'Product & Engineering',
|
|
66
|
+
'Personal',
|
|
67
|
+
] as const;
|
|
68
|
+
|
|
69
|
+
export type NotisAppCategory = typeof NOTIS_APP_CATEGORIES[number];
|
|
70
|
+
|
|
71
|
+
export interface NotisAppDatabaseConfig {
|
|
72
|
+
/** Database slug, as declared by the app. */
|
|
73
|
+
slug: string;
|
|
74
|
+
/**
|
|
75
|
+
* Ship this database's rows to everyone who installs the app.
|
|
76
|
+
*
|
|
77
|
+
* Off by default: declaring a database publishes its STRUCTURE, never its
|
|
78
|
+
* content. Turn it on only for starter content that every installer should
|
|
79
|
+
* receive - a default folder tree, a set of templates - and never for a
|
|
80
|
+
* database that accumulates the author's own data.
|
|
81
|
+
*/
|
|
82
|
+
seedDocuments?: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export interface NotisAppAuthor {
|
|
86
|
+
name: string;
|
|
87
|
+
handle?: string;
|
|
88
|
+
url?: string;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface NotisAppSkillConfig {
|
|
92
|
+
/** Stable source-owned key used by other app declarations. */
|
|
93
|
+
key: string;
|
|
94
|
+
/**
|
|
95
|
+
* Path to the skill, relative to notis.config.ts. Either a Markdown file
|
|
96
|
+
* (`./skills/onboarding.md`) or a directory holding SKILL.md plus its
|
|
97
|
+
* supporting files (`./skills/onboarding/`), which are packaged on deploy
|
|
98
|
+
* and materialized next to SKILL.md in the sandbox.
|
|
99
|
+
*/
|
|
100
|
+
path: string;
|
|
101
|
+
/** User-facing name used for the installed skill. */
|
|
102
|
+
name: string;
|
|
103
|
+
description?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface NotisAppOnboardingConfig {
|
|
107
|
+
/** Key of a skill declared in `skills`. */
|
|
108
|
+
skill: string;
|
|
109
|
+
/** Editable message placed in Notis when onboarding is opened. */
|
|
110
|
+
prompt: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface NotisAppScreenshotConfig {
|
|
114
|
+
/** Conventional metadata/screenshot-N.png source path. */
|
|
115
|
+
path: string;
|
|
116
|
+
/** Meaningful description used by the Store gallery and assistive technology. */
|
|
117
|
+
alt: string;
|
|
118
|
+
/** Route slug captured by `notis apps screenshot`. */
|
|
119
|
+
route?: string;
|
|
120
|
+
/**
|
|
121
|
+
* Named scenario from metadata/screenshot-fixtures.json. Its `tools` and
|
|
122
|
+
* `requests` override the file-level ones key by key for this capture, and
|
|
123
|
+
* its `actions` run once the route has mounted -- so one route can be shown
|
|
124
|
+
* in several states (populated, empty, a panel opened) without the states
|
|
125
|
+
* leaking into each other.
|
|
126
|
+
*/
|
|
127
|
+
scenario?: string;
|
|
128
|
+
/** Optional CSS selector captured as the truthful focal region for this Store image. */
|
|
129
|
+
focus?: string;
|
|
130
|
+
/** Portal color scheme used while rendering this screenshot. Defaults to light. */
|
|
131
|
+
theme?: 'light' | 'dark';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Named accent tokens for an app's avatar. Keep in sync with the portal
|
|
136
|
+
* `ACCENT_NAMES` and the server `ACCENT_TOKENS`.
|
|
137
|
+
*/
|
|
138
|
+
export const NOTIS_APP_ACCENTS = [
|
|
139
|
+
'blue',
|
|
140
|
+
'violet',
|
|
141
|
+
'emerald',
|
|
142
|
+
'amber',
|
|
143
|
+
'rose',
|
|
144
|
+
'sky',
|
|
145
|
+
'fuchsia',
|
|
146
|
+
'teal',
|
|
147
|
+
] as const;
|
|
148
|
+
|
|
149
|
+
export type NotisAppAccent = typeof NOTIS_APP_ACCENTS[number];
|
|
150
|
+
|
|
151
|
+
export interface NotisAppCapabilities {
|
|
152
|
+
/**
|
|
153
|
+
* Read every database in the workspace, not only the ones this app declares
|
|
154
|
+
* in `databases` or created itself.
|
|
155
|
+
*
|
|
156
|
+
* An app runtime is otherwise sandboxed to its own databases, so a catalog or
|
|
157
|
+
* explorer app sees an empty list without this. `'read'` is the only accepted
|
|
158
|
+
* value and it never grants writes: `LOCAL_NOTIS_DATABASE_UPSERT_*` stays
|
|
159
|
+
* bound to the app's own databases.
|
|
160
|
+
*/
|
|
161
|
+
workspaceDatabases?: 'read';
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Read a few facts about the user's cloud computer: whether a sandbox exists
|
|
165
|
+
* and is running, and whether the GitHub CLI is signed in there.
|
|
166
|
+
*
|
|
167
|
+
* Without this an app has to infer them — the Workspaces app treated a
|
|
168
|
+
* configured repository as proof that `gh auth login` had happened, which
|
|
169
|
+
* cannot show an account name and cannot notice a revoked credential.
|
|
170
|
+
* `'read'` never creates, resumes or commands a sandbox. Read it with
|
|
171
|
+
* `useCloudComputer()`.
|
|
172
|
+
*
|
|
173
|
+
* `'shell'` additionally asks to command the cloud computer: it unlocks
|
|
174
|
+
* `LOCAL_NOTIS_RUN_SANDBOX_SHELL` and the sandbox file tools from this app's
|
|
175
|
+
* views (they are denied to every view otherwise), and implies the read
|
|
176
|
+
* facts. This is the same authority the user's own agent has on the sandbox,
|
|
177
|
+
* so the user is asked for it explicitly at install or in the Store grant
|
|
178
|
+
* step; declare it only when the app's core actions genuinely run there.
|
|
179
|
+
*/
|
|
180
|
+
cloudComputer?: 'read' | 'shell';
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Execution hint for a tool whose public name is generated by a provider.
|
|
185
|
+
*
|
|
186
|
+
* MCP public names may be truncated or deduplicated, so the upstream action
|
|
187
|
+
* cannot always be reconstructed from the final `LOCAL_MCP_*` name. Binding
|
|
188
|
+
* the two lets the host skip provider schema discovery on direct calls while
|
|
189
|
+
* the public name remains the user-approved permission boundary.
|
|
190
|
+
*/
|
|
191
|
+
export interface NotisAppToolBinding {
|
|
192
|
+
/** Exact final name also present in `tools`. */
|
|
193
|
+
name: string;
|
|
194
|
+
/** Exact upstream MCP action name, for example `execute_sql`. */
|
|
195
|
+
providerToolName: string;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export interface NotisAppConfig {
|
|
199
|
+
/** Standalone reports share the view engine without app installation. */
|
|
200
|
+
kind?: 'app' | 'report';
|
|
201
|
+
/** Existing data sources, never owned or materialized by a report. */
|
|
202
|
+
databaseAccess?: Array<{ id: string; access: 'read' | 'write' }>;
|
|
203
|
+
/** URL-safe app slug. Existing apps may still use a display name here. */
|
|
204
|
+
name: string;
|
|
205
|
+
/** Human display title, Raycast-style. Falls back to `name`. */
|
|
206
|
+
title?: string;
|
|
207
|
+
description?: string;
|
|
208
|
+
/**
|
|
209
|
+
* App icon. A `phosphor:<name>` value (e.g. `phosphor:dice-five`) or
|
|
210
|
+
* `metadata/icon.png`. When unset, the app shows its two-letter initials.
|
|
211
|
+
*/
|
|
212
|
+
icon?: string;
|
|
213
|
+
/**
|
|
214
|
+
* Optional accent color for the app avatar. One of {@link NOTIS_APP_ACCENTS}.
|
|
215
|
+
* When unset, a stable accent is derived automatically from the app id.
|
|
216
|
+
*/
|
|
217
|
+
accent?: NotisAppAccent;
|
|
218
|
+
author?: NotisAppAuthor;
|
|
219
|
+
categories?: NotisAppCategory[];
|
|
220
|
+
tagline?: string;
|
|
221
|
+
/** @deprecated Add release entries to the root CHANGELOG.md instead. */
|
|
222
|
+
versionNotes?: string;
|
|
223
|
+
/** Editorial screenshot order and capture scenarios for the Store listing. */
|
|
224
|
+
screenshots?: NotisAppScreenshotConfig[];
|
|
225
|
+
/**
|
|
226
|
+
* Databases this app owns. A bare string publishes structure only; use the
|
|
227
|
+
* object form to opt a database into shipping its rows to installers.
|
|
228
|
+
*/
|
|
229
|
+
databases?: (string | NotisAppDatabaseConfig)[];
|
|
230
|
+
/**
|
|
231
|
+
* Extra permissions the app asks for at install time. Everything here widens
|
|
232
|
+
* what the app can reach beyond its own data, so each one is surfaced to the
|
|
233
|
+
* user before they install.
|
|
234
|
+
*/
|
|
235
|
+
capabilities?: NotisAppCapabilities;
|
|
236
|
+
routes?: NotisRouteConfig[];
|
|
237
|
+
/**
|
|
238
|
+
* Final tool names this app can call at runtime, enforced server-side. Use
|
|
239
|
+
* names returned by shared discovery, including native `LOCAL_NOTIS_*`,
|
|
240
|
+
* connected-service names such as `GMAIL_SEND_EMAIL`,
|
|
241
|
+
* `LOCAL_POSTFORME_*`, and `LOCAL_MCP_<SERVER>_<TOOL>`. App code calls
|
|
242
|
+
* each declared name directly with `useTool`; metered calls use the shared
|
|
243
|
+
* credit-cap and usage-billing path.
|
|
244
|
+
*/
|
|
245
|
+
tools?: string[];
|
|
246
|
+
/** Optional execution bindings for provider-generated tool names. */
|
|
247
|
+
toolBindings?: NotisAppToolBinding[];
|
|
248
|
+
/** Skills shipped from this app's source tree. */
|
|
249
|
+
skills?: NotisAppSkillConfig[];
|
|
250
|
+
/** Optional onboarding entrypoint exposed from the app sidebar. */
|
|
251
|
+
onboarding?: NotisAppOnboardingConfig;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Identity function that provides type checking and autocomplete for the
|
|
256
|
+
* Notis app configuration. The returned object is read at build time by
|
|
257
|
+
* `notis apps build` to generate the manifest.
|
|
258
|
+
*/
|
|
259
|
+
export function defineNotisApp(config: NotisAppConfig): NotisAppConfig {
|
|
260
|
+
return config;
|
|
261
|
+
}
|