@notis_ai/cli 0.2.0-beta.16.1 → 0.2.0-beta.160.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 +433 -133
- 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 +18672 -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 +16 -6
- 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 +716 -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/layout.tsx +5 -2
- 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/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 +4137 -0
- package/template/package.json +5 -5
- package/template/packages/{notis-sdk → sdk}/package.json +13 -3
- 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 +213 -0
- package/template/packages/sdk/src/tailwind.ts +56 -0
- package/template/packages/{notis-sdk → sdk}/src/vite.ts +5 -1
- package/template/tailwind.config.ts +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 → 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
- /package/template/packages/{notis-sdk → sdk}/tsconfig.json +0 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>Notis App Harness</title>
|
|
6
|
+
<script>
|
|
7
|
+
const harnessTheme = new URLSearchParams(window.location.search).get('theme') === 'dark' ? 'dark' : 'light';
|
|
8
|
+
document.documentElement.classList.toggle('dark', harnessTheme === 'dark');
|
|
9
|
+
document.documentElement.dataset.theme = harnessTheme;
|
|
10
|
+
</script>
|
|
11
|
+
<link rel="stylesheet" href="./bundle/app.css">
|
|
12
|
+
<script type="importmap">
|
|
13
|
+
{
|
|
14
|
+
"imports": {
|
|
15
|
+
"react": "https://esm.sh/react@{{REACT_VERSION}}",
|
|
16
|
+
"react-dom": "https://esm.sh/react-dom@{{REACT_VERSION}}",
|
|
17
|
+
"react/jsx-runtime": "https://esm.sh/react@{{REACT_VERSION}}/jsx-runtime",
|
|
18
|
+
"react-dom/client": "https://esm.sh/react-dom@{{REACT_VERSION}}/client"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
</script>
|
|
22
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
23
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
24
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
|
|
25
|
+
<style>
|
|
26
|
+
/*
|
|
27
|
+
* Portal-parity theme tokens. In production the portal copies its computed
|
|
28
|
+
* :root custom properties onto the app root, so app CSS written against
|
|
29
|
+
* hsl(var(--token)) resolves to the real portal palette. The harness mirrors
|
|
30
|
+
* the portal's light theme here so dev renders and listing screenshots look
|
|
31
|
+
* exactly like the app inside the portal. Keep in sync with
|
|
32
|
+
* portal/src/app/globals.css (:root).
|
|
33
|
+
*/
|
|
34
|
+
:root {
|
|
35
|
+
color-scheme: light;
|
|
36
|
+
--background: 0 0% 100%;
|
|
37
|
+
--foreground: 222.2 84% 4.9%;
|
|
38
|
+
--card: 0 0% 100%;
|
|
39
|
+
--card-foreground: 222.2 84% 4.9%;
|
|
40
|
+
--popover: 0 0% 100%;
|
|
41
|
+
--popover-foreground: 222.2 84% 4.9%;
|
|
42
|
+
--primary: 222.2 47.4% 11.2%;
|
|
43
|
+
--primary-foreground: 210 40% 98%;
|
|
44
|
+
--secondary: 210 40% 96.1%;
|
|
45
|
+
--secondary-foreground: 222.2 47.4% 11.2%;
|
|
46
|
+
--muted: 210 40% 96.1%;
|
|
47
|
+
--muted-foreground: 0 0% 46.9%;
|
|
48
|
+
--accent: 210 40% 96.1%;
|
|
49
|
+
--accent-foreground: 222.2 47.4% 11.2%;
|
|
50
|
+
--destructive: 0 84.2% 60.2%;
|
|
51
|
+
--destructive-foreground: 210 40% 98%;
|
|
52
|
+
--border: 214.3 31.8% 91.4%;
|
|
53
|
+
--input: 214.3 31.8% 91.4%;
|
|
54
|
+
--ring: 222.2 84% 4.9%;
|
|
55
|
+
--radius: 0.5rem;
|
|
56
|
+
--chart-1: 210 100% 49%;
|
|
57
|
+
--chart-2: 173 58% 39%;
|
|
58
|
+
--chart-3: 197 37% 24%;
|
|
59
|
+
--chart-4: 43 74% 66%;
|
|
60
|
+
--chart-5: 27 87% 67%;
|
|
61
|
+
}
|
|
62
|
+
:root.dark {
|
|
63
|
+
color-scheme: dark;
|
|
64
|
+
--background: 0 0% 13%;
|
|
65
|
+
--foreground: 210 40% 98%;
|
|
66
|
+
--card: 0 0% 13%;
|
|
67
|
+
--card-foreground: 210 40% 98%;
|
|
68
|
+
--popover: 0 0% 13%;
|
|
69
|
+
--popover-foreground: 210 40% 98%;
|
|
70
|
+
--primary: 210 40% 98%;
|
|
71
|
+
--primary-foreground: 222.2 47.4% 11.2%;
|
|
72
|
+
--secondary: 0 0% 18%;
|
|
73
|
+
--secondary-foreground: 210 40% 98%;
|
|
74
|
+
--muted: 0 0% 18%;
|
|
75
|
+
--muted-foreground: 0 0% 65.1%;
|
|
76
|
+
--accent: 0 0% 18%;
|
|
77
|
+
--accent-foreground: 210 40% 98%;
|
|
78
|
+
--destructive: 0 84.2% 60.2%;
|
|
79
|
+
--destructive-foreground: 210 40% 98%;
|
|
80
|
+
--border: 0 0% 23.5%;
|
|
81
|
+
--input: 0 0% 23.5%;
|
|
82
|
+
--ring: 212.7 26.8% 83.9%;
|
|
83
|
+
--chart-1: 220 70% 50%;
|
|
84
|
+
--chart-2: 160 60% 45%;
|
|
85
|
+
--chart-3: 30 80% 55%;
|
|
86
|
+
--chart-4: 280 65% 60%;
|
|
87
|
+
--chart-5: 340 75% 55%;
|
|
88
|
+
}
|
|
89
|
+
body {
|
|
90
|
+
margin: 0;
|
|
91
|
+
font-family: 'Inter', system-ui, -apple-system, sans-serif;
|
|
92
|
+
background: hsl(var(--background));
|
|
93
|
+
color: hsl(var(--foreground));
|
|
94
|
+
-webkit-font-smoothing: antialiased;
|
|
95
|
+
}
|
|
96
|
+
#harness-status { position: fixed; top: 0; left: 0; right: 0; padding: 4px 8px; font-size: 11px; background: #111; color: #0f0; font-family: ui-monospace, SFMono-Regular, Menlo, monospace; z-index: 9999; }
|
|
97
|
+
#root { padding-top: 24px; min-height: 100vh; }
|
|
98
|
+
</style>
|
|
99
|
+
</head>
|
|
100
|
+
<body>
|
|
101
|
+
<div id="harness-status">harness: booting</div>
|
|
102
|
+
<div id="root"></div>
|
|
103
|
+
<script type="module">
|
|
104
|
+
const routeExport = {{ROUTE_EXPORT}};
|
|
105
|
+
const descriptor = {{RUNTIME_DESCRIPTOR}};
|
|
106
|
+
const mode = {{MODE}};
|
|
107
|
+
const apiBase = {{API_BASE}};
|
|
108
|
+
const jwt = {{JWT}};
|
|
109
|
+
const fixtures = {{FIXTURES}};
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Resolves the canned response for one tool call.
|
|
113
|
+
*
|
|
114
|
+
* A plain `"TOOL_NAME"` key answers every call to that tool. An app that
|
|
115
|
+
* reaches several databases through a single tool needs to vary the
|
|
116
|
+
* payload per call, so a key may also be scoped to one argument with
|
|
117
|
+
* `"TOOL_NAME#arg=value"` — for example
|
|
118
|
+
* `"LOCAL_NOTIS_DATABASE_QUERY#database_slug=notes"`. Scoped keys are
|
|
119
|
+
* checked first; the unscoped key stays the fallback.
|
|
120
|
+
*/
|
|
121
|
+
function lookupToolFixture(name, args) {
|
|
122
|
+
const tools = fixtures && fixtures.tools;
|
|
123
|
+
if (!tools) return undefined;
|
|
124
|
+
for (const key of Object.keys(tools)) {
|
|
125
|
+
const sep = key.indexOf('#');
|
|
126
|
+
if (sep === -1 || key.slice(0, sep) !== name) continue;
|
|
127
|
+
const scope = key.slice(sep + 1);
|
|
128
|
+
const eq = scope.indexOf('=');
|
|
129
|
+
if (eq === -1) continue;
|
|
130
|
+
if (String(args[scope.slice(0, eq)]) === scope.slice(eq + 1)) {
|
|
131
|
+
return tools[key];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return Object.prototype.hasOwnProperty.call(tools, name) ? tools[name] : undefined;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const status = document.getElementById('harness-status');
|
|
138
|
+
const setStatus = (msg, color = '#0f0') => {
|
|
139
|
+
status.textContent = `harness: ${msg}`;
|
|
140
|
+
status.style.color = color;
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
window.__harness = {
|
|
144
|
+
errors: [],
|
|
145
|
+
runtimeCalls: [],
|
|
146
|
+
renderStarted: false,
|
|
147
|
+
mounted: false,
|
|
148
|
+
mode,
|
|
149
|
+
route: descriptor.route && descriptor.route.slug,
|
|
150
|
+
scenario: descriptor.context && descriptor.context.screenshotScenario,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
window.addEventListener('error', (event) => {
|
|
154
|
+
window.__harness.errors.push({
|
|
155
|
+
type: 'window.error',
|
|
156
|
+
message: event.message,
|
|
157
|
+
stack: event.error && event.error.stack,
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
window.addEventListener('unhandledrejection', (event) => {
|
|
161
|
+
window.__harness.errors.push({
|
|
162
|
+
type: 'unhandledrejection',
|
|
163
|
+
reason: String((event.reason && event.reason.stack) || event.reason),
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Records one runtime call and hands back the entry so the caller can
|
|
169
|
+
* stamp its outcome once the promise settles. Verify reads `ok` to tell a
|
|
170
|
+
* route that rendered from real data apart from one that only rendered its
|
|
171
|
+
* error states, so every runtime path must settle its own entry.
|
|
172
|
+
*/
|
|
173
|
+
function record(op, args) {
|
|
174
|
+
const entry = { op, args, ok: null, error: null, durationMs: null };
|
|
175
|
+
entry.startedAt = Date.now();
|
|
176
|
+
window.__harness.runtimeCalls.push(entry);
|
|
177
|
+
return entry;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function settle(entry, error) {
|
|
181
|
+
entry.durationMs = Date.now() - entry.startedAt;
|
|
182
|
+
delete entry.startedAt;
|
|
183
|
+
entry.ok = !error;
|
|
184
|
+
entry.error = error ? (error.message ? String(error.message) : String(error)) : null;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function tracked(entry, run) {
|
|
188
|
+
try {
|
|
189
|
+
const value = await run();
|
|
190
|
+
settle(entry, null);
|
|
191
|
+
return value;
|
|
192
|
+
} catch (error) {
|
|
193
|
+
settle(entry, error);
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
async function readJsonOrSnippet(response) {
|
|
199
|
+
const text = await response.text().catch(() => '');
|
|
200
|
+
if (!text) return { payload: null, snippet: null };
|
|
201
|
+
try {
|
|
202
|
+
return { payload: JSON.parse(text), snippet: null };
|
|
203
|
+
} catch {
|
|
204
|
+
const trimmed = text.trim();
|
|
205
|
+
return { payload: null, snippet: trimmed.length > 120 ? `${trimmed.slice(0, 120)}...` : trimmed };
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function runtimeQuery(body) {
|
|
210
|
+
if (!apiBase || !jwt || !descriptor.app || !descriptor.app.id) {
|
|
211
|
+
throw new Error('live harness mode requires apiBase, jwt, and app id');
|
|
212
|
+
}
|
|
213
|
+
const response = await fetch(`${String(apiBase).replace(/\/$/, '')}/portal_views/runtime_query`, {
|
|
214
|
+
method: 'POST',
|
|
215
|
+
headers: {
|
|
216
|
+
'Content-Type': 'application/json',
|
|
217
|
+
Authorization: `Bearer ${jwt}`,
|
|
218
|
+
},
|
|
219
|
+
body: JSON.stringify({
|
|
220
|
+
app_id: descriptor.app.id,
|
|
221
|
+
route_slug: descriptor.route && descriptor.route.slug,
|
|
222
|
+
...body,
|
|
223
|
+
}),
|
|
224
|
+
});
|
|
225
|
+
const { payload, snippet } = await readJsonOrSnippet(response);
|
|
226
|
+
if (!response.ok) {
|
|
227
|
+
const message = (payload && (payload.error || payload.message)) || snippet || `status ${response.status}`;
|
|
228
|
+
throw new Error(`Runtime request failed: ${message}`);
|
|
229
|
+
}
|
|
230
|
+
if (!payload) {
|
|
231
|
+
throw new Error(snippet ? `Runtime response was not JSON: ${snippet}` : 'Runtime response was empty');
|
|
232
|
+
}
|
|
233
|
+
if (payload.status === 'error' || payload.error) {
|
|
234
|
+
throw new Error(payload.message || payload.error || 'Runtime request failed');
|
|
235
|
+
}
|
|
236
|
+
return payload;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function stubRuntime() {
|
|
240
|
+
return {
|
|
241
|
+
app: descriptor.app,
|
|
242
|
+
route: descriptor.route,
|
|
243
|
+
context: descriptor.context || {},
|
|
244
|
+
navigate: (args) => {
|
|
245
|
+
settle(record('navigate', args), null);
|
|
246
|
+
},
|
|
247
|
+
// There is no manager chat here, so the handover is recorded and
|
|
248
|
+
// reported as drafted without any UI.
|
|
249
|
+
handover: async (payload) => {
|
|
250
|
+
settle(record('handover', payload), null);
|
|
251
|
+
return { status: 'drafted' };
|
|
252
|
+
},
|
|
253
|
+
registerTopBarSearch: () => {},
|
|
254
|
+
setTopBarSearchValue: () => {},
|
|
255
|
+
setTopBarSearchLoading: () => {},
|
|
256
|
+
// No change feed here, so `useDatabaseSubscription` reports live=false
|
|
257
|
+
// and the app keeps whatever manual refresh it offers.
|
|
258
|
+
subscribeDatabase: () => () => {},
|
|
259
|
+
// There is no cloud computer behind the harness. Answering "not
|
|
260
|
+
// available" rather than throwing keeps `useCloudComputer` on the same
|
|
261
|
+
// fallback branch a user without a cloud computer would see.
|
|
262
|
+
cloudComputerFacts: async () => tracked(
|
|
263
|
+
record('cloudComputerFacts', {}),
|
|
264
|
+
async () => ({
|
|
265
|
+
available: false,
|
|
266
|
+
reason: 'unsupported_host',
|
|
267
|
+
sandbox: null,
|
|
268
|
+
cli_auth: {
|
|
269
|
+
gh: { authenticated: null, account: null, checked_at: null, reason: 'unsupported_host' },
|
|
270
|
+
},
|
|
271
|
+
}),
|
|
272
|
+
),
|
|
273
|
+
listTools: async () => tracked(
|
|
274
|
+
record('listTools', {}),
|
|
275
|
+
async () => descriptor.tools || [],
|
|
276
|
+
),
|
|
277
|
+
callTool: async (name, args) => tracked(
|
|
278
|
+
record('callTool', { name, arguments: args || {} }),
|
|
279
|
+
async () => {
|
|
280
|
+
const fixture = lookupToolFixture(name, args || {});
|
|
281
|
+
if (fixture !== undefined) {
|
|
282
|
+
return structuredClone(fixture);
|
|
283
|
+
}
|
|
284
|
+
return { ok: true, result: null };
|
|
285
|
+
},
|
|
286
|
+
),
|
|
287
|
+
request: async (path, options) => tracked(
|
|
288
|
+
record('request', { path, options }),
|
|
289
|
+
async () => {
|
|
290
|
+
if (fixtures && fixtures.requests && Object.prototype.hasOwnProperty.call(fixtures.requests, path)) {
|
|
291
|
+
return structuredClone(fixtures.requests[path]);
|
|
292
|
+
}
|
|
293
|
+
return null;
|
|
294
|
+
},
|
|
295
|
+
),
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
300
|
+
|
|
301
|
+
async function applyScenarioActions() {
|
|
302
|
+
const actions = fixtures && fixtures.scenario && Array.isArray(fixtures.scenario.actions)
|
|
303
|
+
? fixtures.scenario.actions
|
|
304
|
+
: [];
|
|
305
|
+
for (const action of actions) {
|
|
306
|
+
if (action && action.type === 'wait') {
|
|
307
|
+
await delay(Math.max(0, Number(action.ms) || 0));
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (action && action.type === 'click' && typeof action.selector === 'string') {
|
|
311
|
+
const deadline = Date.now() + 5000;
|
|
312
|
+
let element = null;
|
|
313
|
+
while (!element && Date.now() < deadline) {
|
|
314
|
+
element = document.querySelector(action.selector);
|
|
315
|
+
if (!element) await delay(50);
|
|
316
|
+
}
|
|
317
|
+
if (!element || typeof element.click !== 'function') {
|
|
318
|
+
throw new Error(`Screenshot scenario could not click ${action.selector}`);
|
|
319
|
+
}
|
|
320
|
+
element.click();
|
|
321
|
+
await delay(Number(action.settle_ms) || 350);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function liveRuntime() {
|
|
327
|
+
return {
|
|
328
|
+
...stubRuntime(),
|
|
329
|
+
listTools: async () => tracked(
|
|
330
|
+
record('listTools', {}),
|
|
331
|
+
async () => {
|
|
332
|
+
const result = await runtimeQuery({ method: 'tools/list' });
|
|
333
|
+
return result.tools || [];
|
|
334
|
+
},
|
|
335
|
+
),
|
|
336
|
+
callTool: async (name, args) => tracked(
|
|
337
|
+
record('callTool', { name, arguments: args || {} }),
|
|
338
|
+
async () => runtimeQuery({ method: 'tools/call', name, arguments: args || {} }),
|
|
339
|
+
),
|
|
340
|
+
// Live mode must not inherit the stub's canned answer: it would settle
|
|
341
|
+
// ok: true with fake data and soften the all-calls-failed assertion.
|
|
342
|
+
cloudComputerFacts: async (options) => tracked(
|
|
343
|
+
record('cloudComputerFacts', options || {}),
|
|
344
|
+
async () => runtimeQuery({
|
|
345
|
+
method: 'cloud_computer/facts',
|
|
346
|
+
...(options && options.refresh ? { refresh: true } : {}),
|
|
347
|
+
}),
|
|
348
|
+
),
|
|
349
|
+
request: async (path, options) => tracked(
|
|
350
|
+
record('request', { path, options }),
|
|
351
|
+
async () => {
|
|
352
|
+
const response = await fetch(`${String(apiBase).replace(/\/$/, '')}${path}`, {
|
|
353
|
+
method: (options && options.method) || 'GET',
|
|
354
|
+
headers: {
|
|
355
|
+
Authorization: `Bearer ${jwt}`,
|
|
356
|
+
...((options && options.body) ? { 'Content-Type': 'application/json' } : {}),
|
|
357
|
+
...((options && options.headers) || {}),
|
|
358
|
+
},
|
|
359
|
+
body: options && options.body ? JSON.stringify(options.body) : undefined,
|
|
360
|
+
});
|
|
361
|
+
const { payload, snippet } = await readJsonOrSnippet(response);
|
|
362
|
+
if (!response.ok) {
|
|
363
|
+
throw new Error((payload && (payload.error || payload.message)) || snippet || `Request failed with status ${response.status}`);
|
|
364
|
+
}
|
|
365
|
+
return payload;
|
|
366
|
+
},
|
|
367
|
+
),
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
try {
|
|
372
|
+
setStatus('loading react');
|
|
373
|
+
const React = await import('react');
|
|
374
|
+
const { createRoot } = await import('react-dom/client');
|
|
375
|
+
|
|
376
|
+
setStatus('loading bundle');
|
|
377
|
+
const bundle = await import('./bundle/app.js');
|
|
378
|
+
|
|
379
|
+
setStatus('binding runtime context');
|
|
380
|
+
const RuntimeContext = globalThis[Symbol.for('notis.sdk.runtime_context')];
|
|
381
|
+
if (!RuntimeContext) {
|
|
382
|
+
throw new Error('runtime context Symbol not initialized by bundle');
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
const Page = bundle[routeExport];
|
|
386
|
+
if (!Page) {
|
|
387
|
+
throw new Error(`bundle has no ${routeExport} export`);
|
|
388
|
+
}
|
|
389
|
+
const Shell = bundle.__AppShell;
|
|
390
|
+
const runtime = mode === 'live' ? liveRuntime() : stubRuntime();
|
|
391
|
+
const Root = () =>
|
|
392
|
+
React.createElement(
|
|
393
|
+
RuntimeContext.Provider,
|
|
394
|
+
{ value: runtime },
|
|
395
|
+
Shell
|
|
396
|
+
? React.createElement(Shell, null, React.createElement(Page, null))
|
|
397
|
+
: React.createElement(Page, null),
|
|
398
|
+
);
|
|
399
|
+
|
|
400
|
+
setStatus('mounting');
|
|
401
|
+
window.__harness.renderStarted = true;
|
|
402
|
+
createRoot(document.getElementById('root')).render(React.createElement(Root));
|
|
403
|
+
|
|
404
|
+
requestAnimationFrame(() => requestAnimationFrame(async () => {
|
|
405
|
+
try {
|
|
406
|
+
await delay(500);
|
|
407
|
+
await applyScenarioActions();
|
|
408
|
+
window.__harness.mounted = true;
|
|
409
|
+
const errors = window.__harness.errors.length;
|
|
410
|
+
const calls = window.__harness.runtimeCalls.length;
|
|
411
|
+
setStatus(`mounted (errors=${errors}, runtimeCalls=${calls})`, errors ? '#f55' : '#0f0');
|
|
412
|
+
} catch (error) {
|
|
413
|
+
window.__harness.errors.push({
|
|
414
|
+
type: 'scenario',
|
|
415
|
+
message: error && error.message ? error.message : String(error),
|
|
416
|
+
});
|
|
417
|
+
setStatus(`scenario failed: ${error && error.message ? error.message : String(error)}`, '#f55');
|
|
418
|
+
}
|
|
419
|
+
}));
|
|
420
|
+
} catch (error) {
|
|
421
|
+
window.__harness.errors.push({
|
|
422
|
+
type: 'boot',
|
|
423
|
+
message: error && error.message ? error.message : String(error),
|
|
424
|
+
stack: error && error.stack,
|
|
425
|
+
});
|
|
426
|
+
setStatus(`boot failed: ${error && error.message ? error.message : String(error)}`, '#f55');
|
|
427
|
+
}
|
|
428
|
+
</script>
|
|
429
|
+
</body>
|
|
430
|
+
</html>
|
package/template/app/layout.tsx
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
import { NotisProvider } from '@notis/sdk';
|
|
2
1
|
import '@notis/sdk/styles.css';
|
|
2
|
+
import { ShortcutProvider } from '@notis/sdk/interactions';
|
|
3
3
|
import './globals.css';
|
|
4
4
|
|
|
5
5
|
export default function AppShell({ children }: { children: React.ReactNode }) {
|
|
6
|
-
|
|
6
|
+
// NotisProvider supplies this in the product host. Keeping the shell-level
|
|
7
|
+
// provider makes the same scope arbitration available in the local harness;
|
|
8
|
+
// ShortcutProvider is nesting-safe, so the hosted app still has one registry.
|
|
9
|
+
return <ShortcutProvider>{children}</ShortcutProvider>;
|
|
7
10
|
}
|
package/template/app/page.tsx
CHANGED
|
@@ -1,55 +1,62 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
-
import { useNotis,
|
|
3
|
+
import { getDocumentPreview, useDocuments, useNotis, Skeleton, ViewSkeleton } from '@notis/sdk';
|
|
4
4
|
import { Badge } from '@/components/ui/badge';
|
|
5
|
-
import {
|
|
5
|
+
import { PageHeading } from '@/components/page-heading';
|
|
6
6
|
|
|
7
|
+
// Reference page for the flat Notis design bar: a plain page header, bare
|
|
8
|
+
// figures, and a hairline-free list. No bordered boxes, no dividers, no
|
|
9
|
+
// palette colors, no loading text. `notis apps build` enforces these rules.
|
|
7
10
|
export default function HomePage() {
|
|
8
11
|
const { app, ready } = useNotis();
|
|
9
|
-
|
|
12
|
+
// Documents come back normalized: plain property values, camelCase fields,
|
|
13
|
+
// and typed content (contentMarkdown / contentBlocknote / plainText).
|
|
14
|
+
const { documents, loading, hasData, error, refetch } = useDocuments('items', { pageSize: 25 });
|
|
10
15
|
|
|
11
16
|
return (
|
|
12
|
-
<main className="notis-app-shell space-y-
|
|
13
|
-
<
|
|
14
|
-
<
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
17
|
+
<main className="notis-app-shell space-y-8">
|
|
18
|
+
<PageHeading
|
|
19
|
+
title={ready ? app?.name ?? 'Items' : <Skeleton style={{ width: 160, height: 28 }} />}
|
|
20
|
+
description={ready ? app?.description ?? undefined : <Skeleton style={{ width: 240 }} />}
|
|
21
|
+
/>
|
|
22
|
+
|
|
23
|
+
<section className="grid grid-cols-2 gap-x-6 gap-y-4 sm:grid-cols-4">
|
|
24
|
+
<div className="min-w-0">
|
|
25
|
+
<p className="text-xs font-medium text-muted-foreground">Items</p>
|
|
26
|
+
<p className="mt-1 text-2xl font-semibold tabular-nums">
|
|
27
|
+
{hasData ? documents.length : <Skeleton style={{ width: 48, height: 28 }} />}
|
|
28
|
+
</p>
|
|
29
|
+
</div>
|
|
30
|
+
</section>
|
|
24
31
|
|
|
25
|
-
<
|
|
26
|
-
<
|
|
27
|
-
|
|
28
|
-
<
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
<Badge variant="outline">{String(doc.properties.status)}</Badge>
|
|
45
|
-
) : null}
|
|
46
|
-
</div>
|
|
32
|
+
<section className="space-y-3">
|
|
33
|
+
<h2 className="text-base font-semibold">Recent items</h2>
|
|
34
|
+
{error ? (
|
|
35
|
+
<p role="alert" className="rounded-xl bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
|
36
|
+
{error.message}{' '}
|
|
37
|
+
<button type="button" className="font-medium underline" onClick={refetch}>Retry</button>
|
|
38
|
+
</p>
|
|
39
|
+
) : null}
|
|
40
|
+
{loading ? (
|
|
41
|
+
<ViewSkeleton variant="table" rows={4} />
|
|
42
|
+
) : !hasData ? null : documents.length === 0 ? (
|
|
43
|
+
<p className="px-4 py-10 text-center text-sm text-muted-foreground">No items yet. Create your first item.</p>
|
|
44
|
+
) : (
|
|
45
|
+
<div className="space-y-2 lg:space-y-0">
|
|
46
|
+
{documents.map((doc) => (
|
|
47
|
+
<div key={doc.id} className="list-row flex items-center justify-between gap-3">
|
|
48
|
+
<div className="min-w-0">
|
|
49
|
+
<p className="truncate text-sm font-medium">{doc.title || 'Untitled'}</p>
|
|
50
|
+
<p className="mt-0.5 truncate text-xs text-muted-foreground">{getDocumentPreview(doc)}</p>
|
|
47
51
|
</div>
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
52
|
+
{typeof doc.properties.status === 'string' ? (
|
|
53
|
+
<Badge variant="secondary" className="shrink-0">{doc.properties.status}</Badge>
|
|
54
|
+
) : null}
|
|
55
|
+
</div>
|
|
56
|
+
))}
|
|
57
|
+
</div>
|
|
58
|
+
)}
|
|
59
|
+
</section>
|
|
53
60
|
</main>
|
|
54
61
|
);
|
|
55
62
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
// Mirrors the portal's PortalPageHeader: plain title, one-line description,
|
|
4
|
+
// actions on the right. No eyebrow, no rule underneath.
|
|
5
|
+
export function PageHeading({
|
|
6
|
+
title,
|
|
7
|
+
description,
|
|
8
|
+
actions,
|
|
9
|
+
}: {
|
|
10
|
+
title: ReactNode;
|
|
11
|
+
description?: ReactNode;
|
|
12
|
+
actions?: ReactNode;
|
|
13
|
+
}) {
|
|
14
|
+
return (
|
|
15
|
+
<header className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
|
16
|
+
<div className="min-w-0 flex-1">
|
|
17
|
+
<h1 className="text-2xl font-semibold tracking-tight text-foreground">{title}</h1>
|
|
18
|
+
{description ? <p className="mt-1.5 text-sm text-muted-foreground">{description}</p> : null}
|
|
19
|
+
</div>
|
|
20
|
+
{actions ? <div className="flex shrink-0 flex-wrap items-center gap-2 sm:justify-end">{actions}</div> : null}
|
|
21
|
+
</header>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -4,13 +4,16 @@ import { cva, type VariantProps } from 'class-variance-authority';
|
|
|
4
4
|
import { cn } from '@/lib/utils';
|
|
5
5
|
|
|
6
6
|
const badgeVariants = cva(
|
|
7
|
-
'inline-flex items-center rounded-full
|
|
7
|
+
'inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium transition-colors',
|
|
8
8
|
{
|
|
9
9
|
variants: {
|
|
10
10
|
variant: {
|
|
11
|
-
default: '
|
|
12
|
-
secondary: '
|
|
13
|
-
|
|
11
|
+
default: 'bg-primary/10 text-primary',
|
|
12
|
+
secondary: 'bg-foreground/[0.07] text-foreground',
|
|
13
|
+
destructive: 'bg-destructive/10 text-destructive',
|
|
14
|
+
// `outline` is kept for API compatibility and renders as plain text: the
|
|
15
|
+
// design bar has no outlined badges.
|
|
16
|
+
outline: 'bg-transparent text-foreground',
|
|
14
17
|
},
|
|
15
18
|
},
|
|
16
19
|
defaultVariants: {
|
|
@@ -2,20 +2,33 @@ import * as React from 'react';
|
|
|
2
2
|
|
|
3
3
|
import { cn } from '@/lib/utils';
|
|
4
4
|
|
|
5
|
+
// Surfaces are flat: a tinted panel on the page, and a plain background panel
|
|
6
|
+
// when nested inside another Card. No borders, no dividers, no shadows.
|
|
7
|
+
const CardDepthContext = React.createContext(0);
|
|
8
|
+
|
|
5
9
|
const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
6
|
-
({ className, ...props }, ref) =>
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
10
|
+
({ className, ...props }, ref) => {
|
|
11
|
+
const depth = React.useContext(CardDepthContext);
|
|
12
|
+
return (
|
|
13
|
+
<CardDepthContext.Provider value={depth + 1}>
|
|
14
|
+
<div
|
|
15
|
+
ref={ref}
|
|
16
|
+
className={cn(
|
|
17
|
+
'min-w-0 rounded-2xl text-card-foreground',
|
|
18
|
+
depth === 0 ? 'bg-muted' : 'bg-background',
|
|
19
|
+
className,
|
|
20
|
+
)}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
</CardDepthContext.Provider>
|
|
24
|
+
);
|
|
25
|
+
},
|
|
13
26
|
);
|
|
14
27
|
Card.displayName = 'Card';
|
|
15
28
|
|
|
16
29
|
const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
17
30
|
({ className, ...props }, ref) => (
|
|
18
|
-
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-
|
|
31
|
+
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-5', className)} {...props} />
|
|
19
32
|
),
|
|
20
33
|
);
|
|
21
34
|
CardHeader.displayName = 'CardHeader';
|
|
@@ -24,7 +37,7 @@ const CardTitle = React.forwardRef<HTMLParagraphElement, React.HTMLAttributes<HT
|
|
|
24
37
|
({ className, ...props }, ref) => (
|
|
25
38
|
<h3
|
|
26
39
|
ref={ref}
|
|
27
|
-
className={cn('text-
|
|
40
|
+
className={cn('text-base font-semibold leading-none tracking-tight', className)}
|
|
28
41
|
{...props}
|
|
29
42
|
/>
|
|
30
43
|
),
|
|
@@ -41,14 +54,14 @@ CardDescription.displayName = 'CardDescription';
|
|
|
41
54
|
|
|
42
55
|
const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
43
56
|
({ className, ...props }, ref) => (
|
|
44
|
-
<div ref={ref} className={cn('p-
|
|
57
|
+
<div ref={ref} className={cn('p-5 pt-0', className)} {...props} />
|
|
45
58
|
),
|
|
46
59
|
);
|
|
47
60
|
CardContent.displayName = 'CardContent';
|
|
48
61
|
|
|
49
62
|
const CardFooter = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
|
|
50
63
|
({ className, ...props }, ref) => (
|
|
51
|
-
<div ref={ref} className={cn('flex items-center p-
|
|
64
|
+
<div ref={ref} className={cn('flex items-center p-5 pt-0', className)} {...props} />
|
|
52
65
|
),
|
|
53
66
|
);
|
|
54
67
|
CardFooter.displayName = 'CardFooter';
|