@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.
Files changed (154) hide show
  1. package/README.md +433 -133
  2. package/config/notis_app_boundary_rules.json +50 -0
  3. package/config/notis_app_design_rules.json +135 -0
  4. package/dist/agent-hooks/notis-agent-hook.mjs +18672 -0
  5. package/dist/base-skills/notis-apps/SKILL.md +70 -0
  6. package/dist/base-skills/notis-apps/references/architecture.md +164 -0
  7. package/dist/base-skills/notis-apps/references/context.md +81 -0
  8. package/dist/base-skills/notis-apps/references/design.md +165 -0
  9. package/dist/base-skills/notis-apps/references/reading.md +89 -0
  10. package/dist/base-skills/notis-apps/references/release.md +99 -0
  11. package/dist/base-skills/notis-apps/references/sdk.md +62 -0
  12. package/dist/base-skills/notis-apps/references/troubleshooting.md +23 -0
  13. package/dist/base-skills/notis-cli/SKILL.md +140 -0
  14. package/dist/base-skills/notis-cli/references/app-delivery.md +18 -0
  15. package/dist/base-skills/notis-cli/references/native-databases.md +20 -0
  16. package/dist/base-skills/notis-cli/references/tool-examples.md +56 -0
  17. package/dist/base-skills/notis-cli/references/troubleshooting.md +39 -0
  18. package/dist/base-skills/notis-query/SKILL.md +67 -0
  19. package/dist/base-skills/notis-query/references/database-discovery.md +59 -0
  20. package/dist/base-skills/notis-query/references/documents.md +50 -0
  21. package/dist/base-skills/notis-query/references/query.md +543 -0
  22. package/dist/skill-sync/index.js +1626 -0
  23. package/dist/skill-sync/index.js.map +7 -0
  24. package/dist/skill-sync-worker.mjs +2990 -0
  25. package/package.json +16 -6
  26. package/skills/notis-apps/cli.md +313 -0
  27. package/skills/notis-cli/AGENT_INSTRUCTIONS.md +39 -0
  28. package/skills/notis-onboarding/BRIEF.md +129 -0
  29. package/skills/notis-query/cli.md +39 -0
  30. package/src/agent-hook-entry.js +5 -0
  31. package/src/cli.js +294 -25
  32. package/src/command-specs/agents.js +392 -0
  33. package/src/command-specs/apps.js +1470 -202
  34. package/src/command-specs/auth.js +114 -137
  35. package/src/command-specs/diagnostics.js +716 -0
  36. package/src/command-specs/handover.js +374 -0
  37. package/src/command-specs/helpers.js +84 -82
  38. package/src/command-specs/index.js +25 -6
  39. package/src/command-specs/meta.js +150 -18
  40. package/src/command-specs/onboarding.js +290 -0
  41. package/src/command-specs/profile.js +358 -0
  42. package/src/command-specs/reports.js +86 -0
  43. package/src/command-specs/skills.js +75 -0
  44. package/src/command-specs/smoke.js +386 -0
  45. package/src/command-specs/tools.js +455 -139
  46. package/src/runtime/agent-browser.js +632 -0
  47. package/src/runtime/agent-memory-state.js +126 -0
  48. package/src/runtime/agent-setup.js +383 -0
  49. package/src/runtime/app-boundary-validator.js +404 -0
  50. package/src/runtime/app-changelog.js +79 -0
  51. package/src/runtime/app-platform.js +2633 -210
  52. package/src/runtime/app-registry-scaffolds.js +367 -0
  53. package/src/runtime/app-test-server.js +292 -0
  54. package/src/runtime/assets/store-screenshot-dark.png +0 -0
  55. package/src/runtime/auth-recovery.js +110 -0
  56. package/src/runtime/base-skills.d.ts +20 -0
  57. package/src/runtime/base-skills.js +167 -0
  58. package/src/runtime/channel.js +133 -0
  59. package/src/runtime/delegated-context.js +68 -0
  60. package/src/runtime/errors.js +1 -0
  61. package/src/runtime/git.js +233 -0
  62. package/src/runtime/login-listener.js +15 -0
  63. package/src/runtime/oauth.js +2622 -0
  64. package/src/runtime/output.js +37 -5
  65. package/src/runtime/ports.js +31 -0
  66. package/src/runtime/profiles.js +906 -55
  67. package/src/runtime/skill-sync/cloud-client.ts +99 -0
  68. package/src/runtime/skill-sync/index.ts +697 -0
  69. package/src/runtime/skill-sync/local-scanner.ts +1046 -0
  70. package/src/runtime/skill-sync/symlink-manager.ts +433 -0
  71. package/src/runtime/skill-sync/sync-plan.ts +22 -0
  72. package/src/runtime/skill-sync/types.ts +110 -0
  73. package/src/runtime/skill-sync/write-cloud-skill.ts +50 -0
  74. package/src/runtime/skill-sync-service.js +109 -0
  75. package/src/runtime/store-screenshot.js +143 -0
  76. package/src/runtime/sync-skills.d.ts +37 -0
  77. package/src/runtime/sync-skills.js +231 -0
  78. package/src/runtime/telemetry.js +92 -0
  79. package/src/runtime/transport.js +324 -45
  80. package/src/skill-sync-worker-entry.js +2 -0
  81. package/src/skill-sync-worker.js +50 -0
  82. package/template/.harness/index.html.tmpl +430 -0
  83. package/template/CHANGELOG.md +5 -0
  84. package/template/app/layout.tsx +5 -2
  85. package/template/app/page.tsx +49 -42
  86. package/template/components/page-heading.tsx +23 -0
  87. package/template/components/ui/badge.tsx +7 -4
  88. package/template/components/ui/card.tsx +24 -11
  89. package/template/components/ui/native-select.tsx +24 -0
  90. package/template/notis.config.ts +24 -6
  91. package/template/package-lock.json +4137 -0
  92. package/template/package.json +5 -5
  93. package/template/packages/{notis-sdk → sdk}/package.json +13 -3
  94. package/template/packages/sdk/src/agentContext.ts +36 -0
  95. package/template/packages/sdk/src/components/DocumentEditor.tsx +103 -0
  96. package/template/packages/sdk/src/components/Markdown.tsx +60 -0
  97. package/template/packages/sdk/src/components/MarkdownEditor.tsx +121 -0
  98. package/template/packages/sdk/src/components/MultiSelectActionBar.tsx +285 -0
  99. package/template/packages/sdk/src/components/MultiSelectCheckbox.tsx +97 -0
  100. package/template/packages/sdk/src/components/MultiSelectDragOverlay.tsx +39 -0
  101. package/template/packages/sdk/src/components/NotisCommentBoundary.tsx +172 -0
  102. package/template/packages/sdk/src/components/NotisSelectionBoundary.tsx +59 -0
  103. package/template/packages/sdk/src/components/ShortcutHints.tsx +56 -0
  104. package/template/packages/sdk/src/components/Skeleton.tsx +24 -0
  105. package/template/packages/sdk/src/config.ts +257 -0
  106. package/template/packages/sdk/src/documents.ts +256 -0
  107. package/template/packages/sdk/src/hooks/useActiveResource.ts +19 -0
  108. package/template/packages/sdk/src/hooks/useAgentContext.ts +23 -0
  109. package/template/packages/sdk/src/hooks/useCloudComputer.ts +64 -0
  110. package/template/packages/sdk/src/hooks/useCollectionInteractions.ts +836 -0
  111. package/template/packages/sdk/src/hooks/useDatabaseSchema.ts +49 -0
  112. package/template/packages/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
  113. package/template/packages/sdk/src/hooks/useDocument.ts +43 -0
  114. package/template/packages/sdk/src/hooks/useDocuments.ts +84 -0
  115. package/template/packages/sdk/src/hooks/useHandover.ts +78 -0
  116. package/template/packages/sdk/src/hooks/useLongPressSelection.ts +79 -0
  117. package/template/packages/sdk/src/hooks/useMultiSelect.ts +95 -0
  118. package/template/packages/{notis-sdk → sdk}/src/hooks/useNotis.ts +10 -4
  119. package/template/packages/{notis-sdk → sdk}/src/hooks/useNotisNavigation.ts +11 -8
  120. package/template/packages/sdk/src/hooks/useQuery.ts +71 -0
  121. package/template/packages/sdk/src/hooks/useTool.ts +65 -0
  122. package/template/packages/sdk/src/hooks/useToolQuery.ts +12 -0
  123. package/template/packages/sdk/src/hooks/useTopBarSearch.ts +81 -0
  124. package/template/packages/sdk/src/hooks/useUpsertDocument.ts +95 -0
  125. package/template/packages/sdk/src/index.ts +161 -0
  126. package/template/packages/sdk/src/interactions/actions.ts +59 -0
  127. package/template/packages/sdk/src/interactions/shortcuts.tsx +694 -0
  128. package/template/packages/sdk/src/interactions/visibility.ts +13 -0
  129. package/template/packages/sdk/src/interactions.ts +45 -0
  130. package/template/packages/sdk/src/provider.tsx +44 -0
  131. package/template/packages/sdk/src/queryCache.ts +170 -0
  132. package/template/packages/sdk/src/runtime.ts +451 -0
  133. package/template/packages/sdk/src/styles.css +213 -0
  134. package/template/packages/sdk/src/tailwind.ts +56 -0
  135. package/template/packages/{notis-sdk → sdk}/src/vite.ts +5 -1
  136. package/template/tailwind.config.ts +1 -0
  137. package/src/command-specs/db.js +0 -163
  138. package/src/runtime/app-preview-server.js +0 -312
  139. package/template/packages/notis-sdk/src/config.ts +0 -48
  140. package/template/packages/notis-sdk/src/helpers.ts +0 -131
  141. package/template/packages/notis-sdk/src/hooks/useAppState.ts +0 -50
  142. package/template/packages/notis-sdk/src/hooks/useCollectionItem.ts +0 -58
  143. package/template/packages/notis-sdk/src/hooks/useDatabase.ts +0 -87
  144. package/template/packages/notis-sdk/src/hooks/useDocument.ts +0 -61
  145. package/template/packages/notis-sdk/src/hooks/useTool.ts +0 -49
  146. package/template/packages/notis-sdk/src/hooks/useUpsertDocument.ts +0 -57
  147. package/template/packages/notis-sdk/src/index.ts +0 -47
  148. package/template/packages/notis-sdk/src/provider.tsx +0 -44
  149. package/template/packages/notis-sdk/src/runtime.ts +0 -159
  150. package/template/packages/notis-sdk/src/styles.css +0 -123
  151. /package/template/packages/{notis-sdk → sdk}/src/hooks/useBackend.ts +0 -0
  152. /package/template/packages/{notis-sdk → sdk}/src/hooks/useTools.ts +0 -0
  153. /package/template/packages/{notis-sdk → sdk}/src/ui.ts +0 -0
  154. /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>
@@ -0,0 +1,5 @@
1
+ # App Changelog
2
+
3
+ ## [Initial Release] - {PR_MERGE_DATE}
4
+
5
+ - First Store release.
@@ -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
- return <NotisProvider>{children}</NotisProvider>;
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
  }
@@ -1,55 +1,62 @@
1
1
  'use client';
2
2
 
3
- import { useNotis, useDatabase } from '@notis/sdk';
3
+ import { getDocumentPreview, useDocuments, useNotis, Skeleton, ViewSkeleton } from '@notis/sdk';
4
4
  import { Badge } from '@/components/ui/badge';
5
- import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
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
- const { documents, loading } = useDatabase('items');
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-6">
13
- <Card>
14
- <CardHeader className="space-y-3">
15
- <Badge variant="secondary" className="w-fit">Installed app</Badge>
16
- <div className="space-y-2">
17
- <CardTitle>{ready ? app?.name : 'Loading...'}</CardTitle>
18
- <CardDescription>
19
- {ready ? app?.description : 'Loading app metadata...'}
20
- </CardDescription>
21
- </div>
22
- </CardHeader>
23
- </Card>
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
- <Card>
26
- <CardHeader>
27
- <CardTitle>Items</CardTitle>
28
- <CardDescription>Use shadcn surfaces and portal tokens so the app feels native inside Notis.</CardDescription>
29
- </CardHeader>
30
- <CardContent>
31
- {loading ? (
32
- <p className="text-sm text-muted-foreground">Loading...</p>
33
- ) : documents.length === 0 ? (
34
- <div className="rounded-xl border border-dashed border-border px-4 py-10 text-center text-sm text-muted-foreground">
35
- No items yet. Deploy the app and create some.
36
- </div>
37
- ) : (
38
- <div className="space-y-3">
39
- {documents.map((doc) => (
40
- <div key={doc.id} className="rounded-xl border border-border bg-background px-4 py-3">
41
- <div className="flex items-center justify-between gap-3">
42
- <p className="font-medium">{doc.title}</p>
43
- {doc.properties.status ? (
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
- </div>
50
- )}
51
- </CardContent>
52
- </Card>
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 border px-2.5 py-0.5 text-xs font-medium transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
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: 'border-transparent bg-primary/10 text-primary',
12
- secondary: 'border-transparent bg-secondary text-secondary-foreground',
13
- outline: 'text-foreground',
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
- <div
8
- ref={ref}
9
- className={cn('rounded-2xl border bg-card text-card-foreground shadow-sm', className)}
10
- {...props}
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-6', className)} {...props} />
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-xl font-semibold leading-none tracking-tight', className)}
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-6 pt-0', className)} {...props} />
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-6 pt-0', className)} {...props} />
64
+ <div ref={ref} className={cn('flex items-center p-5 pt-0', className)} {...props} />
52
65
  ),
53
66
  );
54
67
  CardFooter.displayName = 'CardFooter';