@notis_ai/cli 0.2.13 → 0.2.14
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 +45 -3
- package/dist/scaffolds/notis-database/packages/sdk/src/config.ts +40 -2
- package/dist/scaffolds/notis-database/packages/sdk/src/documents.ts +21 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useCloudComputer.ts +97 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/hooks/useHandover.ts +75 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/index.ts +17 -0
- package/dist/scaffolds/notis-database/packages/sdk/src/runtime.ts +132 -1
- package/dist/scaffolds/notis-journal/packages/sdk/src/config.ts +40 -2
- package/dist/scaffolds/notis-journal/packages/sdk/src/documents.ts +21 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useCloudComputer.ts +97 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/hooks/useHandover.ts +75 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/index.ts +17 -0
- package/dist/scaffolds/notis-journal/packages/sdk/src/runtime.ts +132 -1
- package/dist/scaffolds/notis-journal/src/mock-runtime.ts +2 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/config.ts +40 -2
- package/dist/scaffolds/notis-notes/packages/sdk/src/documents.ts +21 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useCloudComputer.ts +97 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/hooks/useHandover.ts +75 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/index.ts +17 -0
- package/dist/scaffolds/notis-notes/packages/sdk/src/runtime.ts +132 -1
- package/dist/scaffolds/notis-random/packages/sdk/src/config.ts +40 -2
- package/dist/scaffolds/notis-random/packages/sdk/src/documents.ts +21 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useCloudComputer.ts +97 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/hooks/useHandover.ts +75 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/index.ts +17 -0
- package/dist/scaffolds/notis-random/packages/sdk/src/runtime.ts +132 -1
- package/package.json +1 -1
- package/skills/notis-apps/SKILL.md +11 -7
- package/skills/notis-apps/cli.md +8 -3
- package/src/command-specs/apps.js +238 -50
- package/src/command-specs/handover.js +374 -0
- package/src/command-specs/index.js +3 -0
- package/src/command-specs/tools.js +6 -0
- package/src/runtime/app-dev-server.js +17 -8
- package/src/runtime/app-platform.js +218 -6
- package/src/runtime/delegated-context.js +68 -0
- package/src/runtime/git.js +233 -0
- package/src/runtime/transport.js +19 -2
- package/template/.harness/index.html.tmpl +116 -47
- package/template/packages/sdk/src/config.ts +52 -0
- package/template/packages/sdk/src/documents.ts +21 -0
- package/template/packages/sdk/src/hooks/useCloudComputer.ts +97 -0
- package/template/packages/sdk/src/hooks/useDatabaseSubscription.ts +76 -0
- package/template/packages/sdk/src/hooks/useHandover.ts +75 -0
- package/template/packages/sdk/src/index.ts +17 -0
- package/template/packages/sdk/src/runtime.ts +132 -1
- package/template/metadata/screenshot-1.png +0 -0
- package/template/metadata/screenshot-2.png +0 -0
- package/template/metadata/screenshot-3.png +0 -0
|
@@ -164,8 +164,35 @@
|
|
|
164
164
|
});
|
|
165
165
|
});
|
|
166
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
|
+
*/
|
|
167
173
|
function record(op, args) {
|
|
168
|
-
|
|
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
|
+
}
|
|
169
196
|
}
|
|
170
197
|
|
|
171
198
|
async function readJsonOrSnippet(response) {
|
|
@@ -214,29 +241,58 @@
|
|
|
214
241
|
app: descriptor.app,
|
|
215
242
|
route: descriptor.route,
|
|
216
243
|
context: descriptor.context || {},
|
|
217
|
-
navigate: (args) =>
|
|
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
|
+
},
|
|
218
253
|
registerTopBarSearch: () => {},
|
|
219
254
|
setTopBarSearchValue: () => {},
|
|
220
255
|
setTopBarSearchLoading: () => {},
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
+
),
|
|
240
296
|
};
|
|
241
297
|
}
|
|
242
298
|
|
|
@@ -270,32 +326,45 @@
|
|
|
270
326
|
function liveRuntime() {
|
|
271
327
|
return {
|
|
272
328
|
...stubRuntime(),
|
|
273
|
-
listTools: async () =>
|
|
274
|
-
record('listTools', {})
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
},
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
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
|
+
),
|
|
299
368
|
};
|
|
300
369
|
}
|
|
301
370
|
|
|
@@ -83,23 +83,45 @@ export interface NotisAppAuthor {
|
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
export interface NotisAppSkillConfig {
|
|
86
|
+
/** Stable source-owned key used by other app declarations. */
|
|
86
87
|
key: string;
|
|
88
|
+
/**
|
|
89
|
+
* Path to the skill, relative to notis.config.ts. Either a Markdown file
|
|
90
|
+
* (`./skills/onboarding.md`) or a directory holding SKILL.md plus its
|
|
91
|
+
* supporting files (`./skills/onboarding/`), which are packaged on deploy
|
|
92
|
+
* and materialized next to SKILL.md in the sandbox.
|
|
93
|
+
*/
|
|
87
94
|
path: string;
|
|
95
|
+
/** User-facing name used for the installed skill. */
|
|
88
96
|
name: string;
|
|
89
97
|
description?: string;
|
|
90
98
|
}
|
|
91
99
|
|
|
92
100
|
export interface NotisAppOnboardingConfig {
|
|
101
|
+
/** Key of a skill declared in `skills`. */
|
|
93
102
|
skill: string;
|
|
103
|
+
/** Editable message placed in Notis when onboarding is opened. */
|
|
94
104
|
prompt: string;
|
|
95
105
|
}
|
|
96
106
|
|
|
97
107
|
export interface NotisAppScreenshotConfig {
|
|
108
|
+
/** Conventional metadata/screenshot-N.png source path. */
|
|
98
109
|
path: string;
|
|
110
|
+
/** Meaningful description used by the Store gallery and assistive technology. */
|
|
99
111
|
alt: string;
|
|
112
|
+
/** Route slug captured by `notis apps screenshot`. */
|
|
100
113
|
route?: string;
|
|
114
|
+
/**
|
|
115
|
+
* Named scenario from metadata/screenshot-fixtures.json. Its `tools` and
|
|
116
|
+
* `requests` override the file-level ones key by key for this capture, and
|
|
117
|
+
* its `actions` run once the route has mounted -- so one route can be shown
|
|
118
|
+
* in several states (populated, empty, a panel opened) without the states
|
|
119
|
+
* leaking into each other.
|
|
120
|
+
*/
|
|
101
121
|
scenario?: string;
|
|
122
|
+
/** Optional CSS selector captured as the truthful focal region for this Store image. */
|
|
102
123
|
focus?: string;
|
|
124
|
+
/** Portal color scheme used while rendering this screenshot. Defaults to light. */
|
|
103
125
|
theme?: 'light' | 'dark';
|
|
104
126
|
}
|
|
105
127
|
|
|
@@ -131,6 +153,25 @@ export interface NotisAppCapabilities {
|
|
|
131
153
|
* bound to the app's own databases.
|
|
132
154
|
*/
|
|
133
155
|
workspaceDatabases?: 'read';
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Read a few facts about the user's cloud computer: whether a sandbox exists
|
|
159
|
+
* and is running, and whether the GitHub CLI is signed in there.
|
|
160
|
+
*
|
|
161
|
+
* Without this an app has to infer them — the Workspaces app treated a
|
|
162
|
+
* configured repository as proof that `gh auth login` had happened, which
|
|
163
|
+
* cannot show an account name and cannot notice a revoked credential.
|
|
164
|
+
* `'read'` never creates, resumes or commands a sandbox. Read it with
|
|
165
|
+
* `useCloudComputer()`.
|
|
166
|
+
*
|
|
167
|
+
* `'shell'` additionally asks to command the cloud computer: it unlocks
|
|
168
|
+
* `LOCAL_NOTIS_RUN_SANDBOX_SHELL` and the sandbox file tools from this app's
|
|
169
|
+
* views (they are denied to every view otherwise), and implies the read
|
|
170
|
+
* facts. This is the same authority the user's own agent has on the sandbox,
|
|
171
|
+
* so the user is asked for it explicitly at install or in the Store grant
|
|
172
|
+
* step; declare it only when the app's core actions genuinely run there.
|
|
173
|
+
*/
|
|
174
|
+
cloudComputer?: 'read' | 'shell';
|
|
134
175
|
}
|
|
135
176
|
|
|
136
177
|
export interface NotisAppConfig {
|
|
@@ -154,6 +195,7 @@ export interface NotisAppConfig {
|
|
|
154
195
|
tagline?: string;
|
|
155
196
|
/** @deprecated Add release entries to the root CHANGELOG.md instead. */
|
|
156
197
|
versionNotes?: string;
|
|
198
|
+
/** Editorial screenshot order and capture scenarios for the Store listing. */
|
|
157
199
|
screenshots?: NotisAppScreenshotConfig[];
|
|
158
200
|
/**
|
|
159
201
|
* Databases this app owns. A bare string publishes structure only; use the
|
|
@@ -167,8 +209,18 @@ export interface NotisAppConfig {
|
|
|
167
209
|
*/
|
|
168
210
|
capabilities?: NotisAppCapabilities;
|
|
169
211
|
routes?: NotisRouteConfig[];
|
|
212
|
+
/**
|
|
213
|
+
* Final tool names this app can call at runtime, enforced server-side. Use
|
|
214
|
+
* names returned by shared discovery, including native `LOCAL_NOTIS_*`,
|
|
215
|
+
* connected-service names such as `GMAIL_SEND_EMAIL`,
|
|
216
|
+
* `LOCAL_POSTFORME_*`, and `LOCAL_MCP_<SERVER>_<TOOL>`. App code calls
|
|
217
|
+
* each declared name directly with `useTool`; metered calls use the shared
|
|
218
|
+
* credit-cap and usage-billing path.
|
|
219
|
+
*/
|
|
170
220
|
tools?: string[];
|
|
221
|
+
/** Skills shipped from this app's source tree. */
|
|
171
222
|
skills?: NotisAppSkillConfig[];
|
|
223
|
+
/** Optional onboarding entrypoint exposed from the app sidebar. */
|
|
172
224
|
onboarding?: NotisAppOnboardingConfig;
|
|
173
225
|
}
|
|
174
226
|
|
|
@@ -12,6 +12,7 @@ import type {
|
|
|
12
12
|
DatabasePropertyType,
|
|
13
13
|
DocumentContentType,
|
|
14
14
|
DocumentRecord,
|
|
15
|
+
SecretPropertyValue,
|
|
15
16
|
} from './runtime';
|
|
16
17
|
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
@@ -45,6 +46,23 @@ export function extractRichText(value: unknown): string {
|
|
|
45
46
|
.join('');
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Reads a `secret` property value. The platform only ever sends the pointer
|
|
51
|
+
* ({present, reference, status, metadata}), so this rebuilds it field by field
|
|
52
|
+
* rather than passing the payload through — an app can never surface secret
|
|
53
|
+
* material through this helper, whatever the server sent.
|
|
54
|
+
*/
|
|
55
|
+
export function getSecretValue(value: unknown): SecretPropertyValue {
|
|
56
|
+
const record = asRecord(value);
|
|
57
|
+
const metadata = asRecord(record?.metadata);
|
|
58
|
+
return {
|
|
59
|
+
present: record?.present === true,
|
|
60
|
+
reference: optionalString(record?.reference),
|
|
61
|
+
status: optionalString(record?.status),
|
|
62
|
+
metadata,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
48
66
|
/** Extracts the ids of a normalized relation property value. */
|
|
49
67
|
export function getRelationIds(value: unknown): string[] {
|
|
50
68
|
if (!Array.isArray(value)) return [];
|
|
@@ -81,6 +99,9 @@ export function normalizePropertyValue(value: unknown): unknown {
|
|
|
81
99
|
return items.map((item) => optionalString(asRecord(item)?.id) ?? item).filter(Boolean);
|
|
82
100
|
}
|
|
83
101
|
if (type === 'date') return optionalString(asRecord(record.date)?.start) ?? record.date ?? null;
|
|
102
|
+
// Before the `type in record` fallthrough: a secret value has no `secret`
|
|
103
|
+
// key, so passing it through would hand the caller the raw payload.
|
|
104
|
+
if (type === 'secret') return getSecretValue(record);
|
|
84
105
|
if (type in record) return record[type];
|
|
85
106
|
return value;
|
|
86
107
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { useNotisRuntime } from '../provider';
|
|
5
|
+
import type { CloudComputerFacts } from '../runtime';
|
|
6
|
+
|
|
7
|
+
export interface UseCloudComputerResult {
|
|
8
|
+
/**
|
|
9
|
+
* The facts, or null while the first read is in flight. `facts.available`
|
|
10
|
+
* is false when this host cannot answer — render the app's own fallback.
|
|
11
|
+
*/
|
|
12
|
+
facts: CloudComputerFacts | null;
|
|
13
|
+
loading: boolean;
|
|
14
|
+
error: Error | null;
|
|
15
|
+
/** Re-read the facts. The platform caches them for a few minutes. */
|
|
16
|
+
refresh: () => Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const UNAVAILABLE: CloudComputerFacts = {
|
|
20
|
+
available: false,
|
|
21
|
+
reason: 'unsupported_host',
|
|
22
|
+
sandbox: null,
|
|
23
|
+
cli_auth: {
|
|
24
|
+
gh: { authenticated: null, account: null, checked_at: null, reason: 'unsupported_host' },
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Read-only facts about the user's cloud computer.
|
|
30
|
+
*
|
|
31
|
+
* ```tsx
|
|
32
|
+
* const { facts } = useCloudComputer();
|
|
33
|
+
* const gh = facts?.available ? facts.cli_auth.gh : null;
|
|
34
|
+
*
|
|
35
|
+
* return gh?.authenticated
|
|
36
|
+
* ? <p>Signed in as {gh.account}</p>
|
|
37
|
+
* : <GithubConnect />;
|
|
38
|
+
* ```
|
|
39
|
+
*
|
|
40
|
+
* Requires `capabilities.cloudComputer: 'read'` in `notis.config.ts` and the
|
|
41
|
+
* user's approval at install time. It answers with state the platform already
|
|
42
|
+
* holds: reading it never creates, resumes or commands a sandbox, and the GitHub
|
|
43
|
+
* probe only runs when the sandbox is already awake. `authenticated: null`
|
|
44
|
+
* therefore means *unknown*, not *signed out* — keep the app's own fallback for
|
|
45
|
+
* that case and for hosts that answer `{ available: false }` (the dev harness,
|
|
46
|
+
* the vite preview).
|
|
47
|
+
*/
|
|
48
|
+
export function useCloudComputer(): UseCloudComputerResult {
|
|
49
|
+
const runtime = useNotisRuntime();
|
|
50
|
+
const [facts, setFacts] = useState<CloudComputerFacts | null>(null);
|
|
51
|
+
// True from the first committed render: the initial read is already queued
|
|
52
|
+
// in an effect, and `{ loading: false, facts: null }` would flash a
|
|
53
|
+
// consumer's fallback branch before the answer arrives.
|
|
54
|
+
const [loading, setLoading] = useState(true);
|
|
55
|
+
const [error, setError] = useState<Error | null>(null);
|
|
56
|
+
const mounted = useRef(true);
|
|
57
|
+
|
|
58
|
+
useEffect(() => {
|
|
59
|
+
mounted.current = true;
|
|
60
|
+
return () => {
|
|
61
|
+
mounted.current = false;
|
|
62
|
+
};
|
|
63
|
+
}, []);
|
|
64
|
+
|
|
65
|
+
const read = useCallback(async (options?: { refresh?: boolean }) => {
|
|
66
|
+
if (!runtime?.cloudComputerFacts) {
|
|
67
|
+
setFacts(UNAVAILABLE);
|
|
68
|
+
setLoading(false);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
setLoading(true);
|
|
73
|
+
setError(null);
|
|
74
|
+
try {
|
|
75
|
+
const next = await runtime.cloudComputerFacts(options);
|
|
76
|
+
if (mounted.current) setFacts(next);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
79
|
+
if (mounted.current) {
|
|
80
|
+
setError(e);
|
|
81
|
+
// A refused or failed read is the same product state as a host that
|
|
82
|
+
// cannot answer: the app shows its fallback instead of an error.
|
|
83
|
+
setFacts(UNAVAILABLE);
|
|
84
|
+
}
|
|
85
|
+
} finally {
|
|
86
|
+
if (mounted.current) setLoading(false);
|
|
87
|
+
}
|
|
88
|
+
}, [runtime]);
|
|
89
|
+
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
void read();
|
|
92
|
+
}, [read]);
|
|
93
|
+
|
|
94
|
+
const refresh = useCallback(() => read({ refresh: true }), [read]);
|
|
95
|
+
|
|
96
|
+
return { facts, loading, error, refresh };
|
|
97
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from 'react';
|
|
4
|
+
import { useNotisRuntime } from '../provider';
|
|
5
|
+
import { useDocuments, type UseDocumentsOptions, type UseDocumentsResult } from './useDocuments';
|
|
6
|
+
import type { DocumentRecord } from '../runtime';
|
|
7
|
+
|
|
8
|
+
export interface UseDatabaseSubscriptionOptions extends UseDocumentsOptions {
|
|
9
|
+
/** Set to false to keep the query but skip the change feed. */
|
|
10
|
+
subscribe?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface UseDatabaseSubscriptionResult extends UseDocumentsResult {
|
|
14
|
+
/** Alias of `documents`, for views that think in rows. */
|
|
15
|
+
rows: DocumentRecord[];
|
|
16
|
+
/** True while a live change feed is attached to this database. */
|
|
17
|
+
live: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Query a Notis database and keep it fresh without polling.
|
|
22
|
+
*
|
|
23
|
+
* ```tsx
|
|
24
|
+
* const { rows, live, refetch } = useDatabaseSubscription('workspaces');
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* A change on the database wakes the hook, which then refetches through the
|
|
28
|
+
* usual `LOCAL_NOTIS_DATABASE_QUERY` path — the change feed is a signal only
|
|
29
|
+
* and never carries row data. Hosts without a change feed (the dev harness,
|
|
30
|
+
* the screenshot stub, the vite preview) still return rows; `live` is false
|
|
31
|
+
* there and the app should keep offering its manual refresh.
|
|
32
|
+
*/
|
|
33
|
+
export function useDatabaseSubscription(
|
|
34
|
+
databaseSlug: string,
|
|
35
|
+
options: UseDatabaseSubscriptionOptions = {},
|
|
36
|
+
): UseDatabaseSubscriptionResult {
|
|
37
|
+
const runtime = useNotisRuntime();
|
|
38
|
+
const { subscribe = true, ...documentOptions } = options;
|
|
39
|
+
const { documents, loading, error, refetch } = useDocuments(databaseSlug, documentOptions);
|
|
40
|
+
const [live, setLive] = useState(false);
|
|
41
|
+
|
|
42
|
+
const refetchRef = useRef(refetch);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
refetchRef.current = refetch;
|
|
45
|
+
}, [refetch]);
|
|
46
|
+
|
|
47
|
+
const enabled = options.enabled !== false && subscribe;
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
if (!runtime?.subscribeDatabase || !enabled || !databaseSlug) {
|
|
51
|
+
setLive(false);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let cancelled = false;
|
|
56
|
+
const unsubscribe = runtime.subscribeDatabase(
|
|
57
|
+
databaseSlug,
|
|
58
|
+
() => {
|
|
59
|
+
refetchRef.current();
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
onStatusChange: (isLive) => {
|
|
63
|
+
if (!cancelled) setLive(isLive);
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
return () => {
|
|
69
|
+
cancelled = true;
|
|
70
|
+
setLive(false);
|
|
71
|
+
unsubscribe?.();
|
|
72
|
+
};
|
|
73
|
+
}, [runtime, databaseSlug, enabled]);
|
|
74
|
+
|
|
75
|
+
return { documents, rows: documents, loading, error, refetch, live };
|
|
76
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useCallback, useState } from 'react';
|
|
4
|
+
import { useNotisRuntime } from '../provider';
|
|
5
|
+
import type { HandoverPayload, HandoverResult } from '../runtime';
|
|
6
|
+
|
|
7
|
+
export interface UseHandoverResult {
|
|
8
|
+
/** Hand the work over. Rejects when the host has no manager chat. */
|
|
9
|
+
handover: (payload: HandoverPayload) => Promise<HandoverResult>;
|
|
10
|
+
/** True while the manager chat is being prepared. */
|
|
11
|
+
pending: boolean;
|
|
12
|
+
error: Error | null;
|
|
13
|
+
/**
|
|
14
|
+
* False when the host cannot hand work over (dev harness, vite preview).
|
|
15
|
+
* Render the app's own fallback — a copyable prompt, say — when it is false.
|
|
16
|
+
*/
|
|
17
|
+
available: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Hand a piece of work from app code to the Notis manager chat.
|
|
22
|
+
*
|
|
23
|
+
* An app displays work; the manager runs it. `handover` puts the message in
|
|
24
|
+
* the chat surface that already owns streaming progress, billing, cancellation
|
|
25
|
+
* and the transcript, and the app watches its own databases for the result.
|
|
26
|
+
*
|
|
27
|
+
* ```tsx
|
|
28
|
+
* const { handover, pending, available } = useHandover();
|
|
29
|
+
*
|
|
30
|
+
* return available ? (
|
|
31
|
+
* <Button
|
|
32
|
+
* disabled={pending}
|
|
33
|
+
* onClick={() => { void handover({ prompt: 'Create a workspace on notis to ...' }); }}
|
|
34
|
+
* >
|
|
35
|
+
* Send to Notis
|
|
36
|
+
* </Button>
|
|
37
|
+
* ) : (
|
|
38
|
+
* <CopyablePrompt prompt="Create a workspace on notis to ..." />
|
|
39
|
+
* );
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* Pass `skill` to bind the work to a skill declared in `notis.config.ts`; the
|
|
43
|
+
* host rejects a key the app does not declare. `autoSend` is accepted for
|
|
44
|
+
* forward compatibility; today's hosts always return `drafted` and let the
|
|
45
|
+
* user press send.
|
|
46
|
+
*/
|
|
47
|
+
export function useHandover(): UseHandoverResult {
|
|
48
|
+
const runtime = useNotisRuntime();
|
|
49
|
+
const [pending, setPending] = useState(false);
|
|
50
|
+
const [error, setError] = useState<Error | null>(null);
|
|
51
|
+
|
|
52
|
+
const handover = useCallback(
|
|
53
|
+
async (payload: HandoverPayload): Promise<HandoverResult> => {
|
|
54
|
+
if (!runtime?.handover) {
|
|
55
|
+
throw new Error('This Notis host cannot hand work to the manager chat.');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
setPending(true);
|
|
59
|
+
setError(null);
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
return await runtime.handover(payload);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
65
|
+
setError(e);
|
|
66
|
+
throw e;
|
|
67
|
+
} finally {
|
|
68
|
+
setPending(false);
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
[runtime],
|
|
72
|
+
);
|
|
73
|
+
|
|
74
|
+
return { handover, pending, error, available: Boolean(runtime?.handover) };
|
|
75
|
+
}
|
|
@@ -13,6 +13,11 @@ export { NotisProvider, useNotisRuntime } from './provider';
|
|
|
13
13
|
export { useNotis } from './hooks/useNotis';
|
|
14
14
|
export { useDocuments } from './hooks/useDocuments';
|
|
15
15
|
export type { UseDocumentsOptions, UseDocumentsResult } from './hooks/useDocuments';
|
|
16
|
+
export { useDatabaseSubscription } from './hooks/useDatabaseSubscription';
|
|
17
|
+
export type {
|
|
18
|
+
UseDatabaseSubscriptionOptions,
|
|
19
|
+
UseDatabaseSubscriptionResult,
|
|
20
|
+
} from './hooks/useDatabaseSubscription';
|
|
16
21
|
export { useDocument } from './hooks/useDocument';
|
|
17
22
|
export type { UseDocumentOptions, UseDocumentResult } from './hooks/useDocument';
|
|
18
23
|
export { useUpsertDocument } from './hooks/useUpsertDocument';
|
|
@@ -22,6 +27,10 @@ export type { UseDatabaseSchemaResult } from './hooks/useDatabaseSchema';
|
|
|
22
27
|
export { useTool } from './hooks/useTool';
|
|
23
28
|
export type { ToolCallState, UseToolResult } from './hooks/useTool';
|
|
24
29
|
export { useTools } from './hooks/useTools';
|
|
30
|
+
export { useHandover } from './hooks/useHandover';
|
|
31
|
+
export type { UseHandoverResult } from './hooks/useHandover';
|
|
32
|
+
export { useCloudComputer } from './hooks/useCloudComputer';
|
|
33
|
+
export type { UseCloudComputerResult } from './hooks/useCloudComputer';
|
|
25
34
|
export { useNotisNavigation } from './hooks/useNotisNavigation';
|
|
26
35
|
export { useTopBarSearch } from './hooks/useTopBarSearch';
|
|
27
36
|
export { useBackend } from './hooks/useBackend';
|
|
@@ -37,6 +46,7 @@ export {
|
|
|
37
46
|
extractRichText,
|
|
38
47
|
getDocumentPreview,
|
|
39
48
|
getRelationIds,
|
|
49
|
+
getSecretValue,
|
|
40
50
|
isPresentString,
|
|
41
51
|
markdownToPlainText,
|
|
42
52
|
normalizeDatabaseProperty,
|
|
@@ -64,6 +74,9 @@ export type { MultiSelectDragOverlayProps } from './components/MultiSelectDragOv
|
|
|
64
74
|
// Types (re-exported for convenience)
|
|
65
75
|
export type {
|
|
66
76
|
AppDescriptor,
|
|
77
|
+
CloudComputerCliAuthFacts,
|
|
78
|
+
CloudComputerFacts,
|
|
79
|
+
CloudComputerSandboxFacts,
|
|
67
80
|
CollectionItem,
|
|
68
81
|
CollectionItemDetail,
|
|
69
82
|
DatabaseDescriptor,
|
|
@@ -72,12 +85,16 @@ export type {
|
|
|
72
85
|
DatabasePropertyType,
|
|
73
86
|
DocumentContentType,
|
|
74
87
|
DocumentRecord,
|
|
88
|
+
HandoverPayload,
|
|
89
|
+
HandoverResult,
|
|
75
90
|
NotisDocumentEditorProps,
|
|
76
91
|
NotisRuntime,
|
|
77
92
|
NotisRuntimeContext,
|
|
78
93
|
NotisRuntimeUI,
|
|
79
94
|
QueryFilter,
|
|
80
95
|
RouteDescriptor,
|
|
96
|
+
SecretPropertyValue,
|
|
97
|
+
SubscribeDatabaseOptions,
|
|
81
98
|
ToolDescriptor,
|
|
82
99
|
ToolInputSchema,
|
|
83
100
|
} from './runtime';
|