@notis_ai/cli 0.2.0-beta.159.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 +94 -0
- package/dist/agent-hooks/notis-agent-hook.mjs +180 -56
- package/dist/base-skills/notis-apps/SKILL.md +21 -2
- package/dist/base-skills/notis-apps/references/context.md +81 -0
- package/dist/base-skills/notis-apps/references/reading.md +89 -0
- package/dist/base-skills/notis-apps/references/sdk.md +1 -0
- package/dist/skill-sync/index.js +25 -6
- package/dist/skill-sync/index.js.map +2 -2
- package/dist/skill-sync-worker.mjs +2 -1
- package/package.json +1 -1
- package/skills/notis-apps/cli.md +89 -0
- package/skills/notis-onboarding/BRIEF.md +6 -5
- package/src/command-specs/apps.js +1 -1
- package/src/command-specs/index.js +3 -0
- package/src/command-specs/onboarding.js +1 -1
- package/src/command-specs/reports.js +86 -0
- package/src/runtime/skill-sync/index.ts +31 -4
- package/template/packages/sdk/src/agentContext.ts +36 -0
- package/template/packages/sdk/src/components/NotisCommentBoundary.tsx +172 -0
- package/template/packages/sdk/src/components/NotisSelectionBoundary.tsx +9 -5
- package/template/packages/sdk/src/hooks/useAgentContext.ts +23 -0
- package/template/packages/sdk/src/index.ts +5 -0
- package/template/packages/sdk/src/runtime.ts +9 -2
- package/template/packages/sdk/src/tailwind.ts +56 -0
- package/template/packages/sdk/src/vite.ts +2 -0
- package/template/tailwind.config.ts +1 -0
package/skills/notis-apps/cli.md
CHANGED
|
@@ -26,6 +26,10 @@ For source restoration, pull the current release into a fresh checkout and histo
|
|
|
26
26
|
another folder. Replace source while retaining the current profile/app link and deployment base,
|
|
27
27
|
change the package release label, check resource compatibility and deploy as a new release.
|
|
28
28
|
|
|
29
|
+
## Reports
|
|
30
|
+
|
|
31
|
+
For a record-owned report, use `reports init → build → verify/preview → save` instead of the app deployment workflow above. Author exactly one route, select an existing app-owned database, and supply a readable context file. Revisions preserve the record ID and require its freshly read revision. See the product `notis-reports` skill for source recovery, ownership and readback. These commands do not deploy the owning app or publish a Store listing.
|
|
32
|
+
|
|
29
33
|
## Commands
|
|
30
34
|
|
|
31
35
|
### `npx --package @notis_ai/cli@latest -- notis doctor`
|
|
@@ -222,3 +226,88 @@ When to use: Diagnose issues with a Notis app project.
|
|
|
222
226
|
Examples:
|
|
223
227
|
- `npx --package @notis_ai/cli@latest -- notis apps doctor`
|
|
224
228
|
- `npx --package @notis_ai/cli@latest -- notis apps doctor ./my-app`
|
|
229
|
+
|
|
230
|
+
### `npx --package @notis_ai/cli@latest -- notis reports init <name> [dir]`
|
|
231
|
+
|
|
232
|
+
Init a record-owned SDK report locally.
|
|
233
|
+
|
|
234
|
+
When to use: Author an independent report without deploying its owning app.
|
|
235
|
+
|
|
236
|
+
Options:
|
|
237
|
+
- `--from <slug>` — Start from a published Store app listed by `notis apps scaffolds list`. Downloads its source from the public app registry.
|
|
238
|
+
|
|
239
|
+
Examples:
|
|
240
|
+
- `npx --package @notis_ai/cli@latest -- notis apps scaffolds list`
|
|
241
|
+
- `npx --package @notis_ai/cli@latest -- notis reports init "Mind the Flo"`
|
|
242
|
+
- `npx --package @notis_ai/cli@latest -- notis reports init "My CRM" --from databases`
|
|
243
|
+
- `npx --package @notis_ai/cli@latest -- notis reports init "My App" ~/code/my-app`
|
|
244
|
+
|
|
245
|
+
### `npx --package @notis_ai/cli@latest -- notis reports build [dir]`
|
|
246
|
+
|
|
247
|
+
Build a record-owned SDK report locally.
|
|
248
|
+
|
|
249
|
+
When to use: Author an independent report without deploying its owning app.
|
|
250
|
+
|
|
251
|
+
Examples:
|
|
252
|
+
- `npx --package @notis_ai/cli@latest -- notis reports build`
|
|
253
|
+
- `npx --package @notis_ai/cli@latest -- notis reports build ./my-app`
|
|
254
|
+
|
|
255
|
+
### `npx --package @notis_ai/cli@latest -- notis reports verify [dir]`
|
|
256
|
+
|
|
257
|
+
Verify a record-owned SDK report locally.
|
|
258
|
+
|
|
259
|
+
When to use: Author an independent report without deploying its owning app.
|
|
260
|
+
|
|
261
|
+
Options:
|
|
262
|
+
- `--routes <slugs>` — Comma-separated route slugs. Default: every route in manifest.
|
|
263
|
+
- `--port <n>` — Loopback port. Default: auto-pick.
|
|
264
|
+
- `--skip-build` — Skip notis apps build; reuse existing .notis/output/.
|
|
265
|
+
- `--mode <mode>` — stub | live. Default stub. Live posts to /portal_views/runtime_query with the CLI JWT and fails routes whose runtime calls all errored.
|
|
266
|
+
- `--listing` — Ignored for reports; saving a report does not publish a Store listing.
|
|
267
|
+
- `--no-browser` — Start the harness server and print URLs; do not drive agent-browser.
|
|
268
|
+
- `--keep-open` — Leave server + browser session running after report (for manual triage).
|
|
269
|
+
|
|
270
|
+
Examples:
|
|
271
|
+
- `npx --package @notis_ai/cli@latest -- notis reports verify`
|
|
272
|
+
- `npx --package @notis_ai/cli@latest -- notis reports verify --routes notes`
|
|
273
|
+
- `npx --package @notis_ai/cli@latest -- notis reports verify --mode live`
|
|
274
|
+
- `npx --package @notis_ai/cli@latest -- notis reports verify --no-browser # start the harness, drive agent-browser yourself`
|
|
275
|
+
|
|
276
|
+
### `npx --package @notis_ai/cli@latest -- notis reports preview [dir]`
|
|
277
|
+
|
|
278
|
+
Preview a record-owned SDK report locally. Keeps the preview server and browser session open.
|
|
279
|
+
|
|
280
|
+
When to use: Author an independent report without deploying its owning app.
|
|
281
|
+
|
|
282
|
+
Options:
|
|
283
|
+
- `--routes <slugs>` — Comma-separated route slugs. Default: every route in manifest.
|
|
284
|
+
- `--port <n>` — Loopback port. Default: auto-pick.
|
|
285
|
+
- `--skip-build` — Skip notis apps build; reuse existing .notis/output/.
|
|
286
|
+
- `--mode <mode>` — stub | live. Default stub. Live posts to /portal_views/runtime_query with the CLI JWT and fails routes whose runtime calls all errored.
|
|
287
|
+
- `--listing` — Ignored for reports; saving a report does not publish a Store listing.
|
|
288
|
+
- `--no-browser` — Start the harness server and print URLs; do not drive agent-browser.
|
|
289
|
+
- `--keep-open` — Leave server + browser session running after report (for manual triage).
|
|
290
|
+
|
|
291
|
+
Examples:
|
|
292
|
+
- `npx --package @notis_ai/cli@latest -- notis reports preview`
|
|
293
|
+
- `npx --package @notis_ai/cli@latest -- notis reports preview --routes notes`
|
|
294
|
+
- `npx --package @notis_ai/cli@latest -- notis reports preview --mode live`
|
|
295
|
+
- `npx --package @notis_ai/cli@latest -- notis reports preview --no-browser # start the harness, drive agent-browser yourself`
|
|
296
|
+
|
|
297
|
+
### `npx --package @notis_ai/cli@latest -- notis reports save [dir]`
|
|
298
|
+
|
|
299
|
+
Build, verify and save a report into an app-owned database record.
|
|
300
|
+
|
|
301
|
+
When to use: Persist an independently authored report, not an app release.
|
|
302
|
+
|
|
303
|
+
Options:
|
|
304
|
+
- `--database-id <id>` — Required. Owning app database.
|
|
305
|
+
- `--document-id <id>` — Existing record to update or attach to.
|
|
306
|
+
- `--attach` — Attach to an existing non-view record.
|
|
307
|
+
- `--expected-revision <revision>` — Fresh view revision (0 for a record without a view).
|
|
308
|
+
- `--title <title>` — Required, including updates. Record title.
|
|
309
|
+
- `--context-file <file>` — Required. UTF-8 readable report content and structure.
|
|
310
|
+
- `--properties-file <file>` — JSON database property values keyed by name.
|
|
311
|
+
|
|
312
|
+
Examples:
|
|
313
|
+
- `npx --package @notis_ai/cli@latest -- notis reports save ./weekly-report --database-id <id> --title "Weekly review" --context-file ./context.md`
|
|
@@ -48,12 +48,12 @@ The fields onboarding collects, wherever it runs. Both the conversational
|
|
|
48
48
|
onboarding assistant and the CLI brief reference this partial so the two paths
|
|
49
49
|
can never quietly collect different things.
|
|
50
50
|
|
|
51
|
-
All of these land through `LOCAL_NOTIS_SAVE_USER_SETTINGS`: `
|
|
51
|
+
All of these land through `LOCAL_NOTIS_SAVE_USER_SETTINGS`: `first_name` on the
|
|
52
52
|
user row, the rest merged into the `settings` blob.
|
|
53
53
|
|
|
54
54
|
| Field | Setting key | What it is for |
|
|
55
55
|
|---|---|---|
|
|
56
|
-
| First name | `
|
|
56
|
+
| First name | `first_name` | How Notis addresses the user. Ask for their given name; preserve compound names and do not infer it from an email address. |
|
|
57
57
|
| Occupation / role | `position` | Tailors examples and suggestions. |
|
|
58
58
|
| Language | `language` | The language Notis replies in. |
|
|
59
59
|
| Time zone | `timezone` | Anchors every scheduled and time-relative request. Ask; never guess silently. |
|
|
@@ -63,8 +63,9 @@ Rules that hold on every surface:
|
|
|
63
63
|
|
|
64
64
|
* Ask one question at a time. A wall of questions reads as a form, and people
|
|
65
65
|
abandon forms.
|
|
66
|
-
*
|
|
67
|
-
|
|
66
|
+
* For first name, skip only when already saved or provided by the user. Never
|
|
67
|
+
deduce a name. For other fields, skip known or confidently deducible values
|
|
68
|
+
and say what you deduced rather than asking the user to confirm a blank.
|
|
68
69
|
* Save as soon as you have the basics rather than batching to the end — a user who
|
|
69
70
|
drops out halfway should not lose what they already told you.
|
|
70
71
|
|
|
@@ -74,7 +75,7 @@ what you inferred, and let the user correct you. Then save:
|
|
|
74
75
|
|
|
75
76
|
```bash
|
|
76
77
|
npx --package @notis_ai/cli@latest -- notis tools exec LOCAL_NOTIS_SAVE_USER_SETTINGS \
|
|
77
|
-
--arguments '{"
|
|
78
|
+
--arguments '{"first_name":"...","position":"...","language":"...","timezone":"..."}'
|
|
78
79
|
```
|
|
79
80
|
|
|
80
81
|
## 2. Connect one app, then immediately read from it
|
|
@@ -786,7 +786,7 @@ async function appsVerifyHandler(ctx) {
|
|
|
786
786
|
});
|
|
787
787
|
|
|
788
788
|
if (keepOpen) {
|
|
789
|
-
process.stderr.write(`[notis apps verify] harness open at ${baseUrl}. Press Ctrl-C to stop.\n`);
|
|
789
|
+
process.stderr.write(`[notis apps verify] harness open at ${urls[0]?.url || baseUrl}. Press Ctrl-C to stop.\n`);
|
|
790
790
|
await new Promise(() => {});
|
|
791
791
|
}
|
|
792
792
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { reportsCommandSpecs } from './reports.js';
|
|
1
2
|
import { appsCommandSpecs } from './apps.js';
|
|
2
3
|
import { toolsCommandSpecs } from './tools.js';
|
|
3
4
|
import { metaCommandSpecs } from './meta.js';
|
|
@@ -11,6 +12,7 @@ import { agentsCommandSpecs } from './agents.js';
|
|
|
11
12
|
import { skillsCommandSpecs } from './skills.js';
|
|
12
13
|
|
|
13
14
|
export const GROUP_SUMMARIES = {
|
|
15
|
+
reports: 'Build and save independent SDK reports into app database records.',
|
|
14
16
|
apps: 'Develop, deploy, and submit Notis Apps.',
|
|
15
17
|
agents: 'Install Notis context into local coding agents.',
|
|
16
18
|
handover: 'Hand the branch you are on to a Notis agent, hosted or your own Codex/Claude.',
|
|
@@ -28,6 +30,7 @@ export const COMMAND_SPECS = [
|
|
|
28
30
|
...agentsCommandSpecs,
|
|
29
31
|
...skillsCommandSpecs,
|
|
30
32
|
...appsCommandSpecs,
|
|
33
|
+
...reportsCommandSpecs,
|
|
31
34
|
...handoverCommandSpecs,
|
|
32
35
|
...toolsCommandSpecs,
|
|
33
36
|
...diagnosticCommandSpecs,
|
|
@@ -157,7 +157,7 @@ async function authenticatedResult(ctx) {
|
|
|
157
157
|
}
|
|
158
158
|
|
|
159
159
|
if (onboardingComplete) {
|
|
160
|
-
const name = state?.settings?.
|
|
160
|
+
const name = state?.settings?.first_name;
|
|
161
161
|
const setupSummary = renderAgentSetup(base.agent_setup);
|
|
162
162
|
return ctx.output.emitSuccess({
|
|
163
163
|
command: 'start',
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { resolve } from 'node:path';
|
|
3
|
+
import { appsCommandSpecs } from './apps.js';
|
|
4
|
+
import { buildArtifact, prepareAppRelease, resolveProjectDir } from '../runtime/app-platform.js';
|
|
5
|
+
import { nextIdempotencyKey, runToolCommand } from './helpers.js';
|
|
6
|
+
import { usageError, EXIT_CODES } from '../runtime/errors.js';
|
|
7
|
+
|
|
8
|
+
const reuse = (command, name = command) => {
|
|
9
|
+
const spec = appsCommandSpecs.find(item => item.command_path.join(' ') === `apps ${command}`);
|
|
10
|
+
return {
|
|
11
|
+
...spec,
|
|
12
|
+
handler: async ctx => {
|
|
13
|
+
const output = new Proxy(ctx.output, {
|
|
14
|
+
get(target, key) {
|
|
15
|
+
if (key === 'emitSuccess') return result => {
|
|
16
|
+
const value = { ...result, warnings: (result.warnings || []).filter(warning => !warning.startsWith('Store readiness:')) };
|
|
17
|
+
if (value.data?.listing) { value.data = { ...value.data }; delete value.data.listing; }
|
|
18
|
+
return target.emitSuccess(value);
|
|
19
|
+
};
|
|
20
|
+
const value = Reflect.get(target, key);
|
|
21
|
+
return typeof value === 'function' ? value.bind(target) : value;
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
return spec.handler({ ...ctx, options: { ...ctx.options, listing: false, ...(name === 'preview' ? { keepOpen: true } : {}) }, output });
|
|
25
|
+
},
|
|
26
|
+
command_path: ['reports', name],
|
|
27
|
+
summary: `${name[0].toUpperCase() + name.slice(1)} a record-owned SDK report locally.${name === 'preview' ? ' Keeps the preview server and browser session open.' : ''}`,
|
|
28
|
+
args_schema: {
|
|
29
|
+
...spec.args_schema,
|
|
30
|
+
options: (spec.args_schema?.options || []).map(option => option.flags === '--listing'
|
|
31
|
+
? { ...option, description: 'Ignored for reports; saving a report does not publish a Store listing.' }
|
|
32
|
+
: option),
|
|
33
|
+
},
|
|
34
|
+
examples: (spec.examples || []).filter(example => !example.includes('--listing')).map(example => example.replace(`apps ${command}`, `reports ${name}`)),
|
|
35
|
+
when_to_use: 'Author an independent report without deploying its owning app.',
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
export const reportsCommandSpecs = [
|
|
39
|
+
reuse('init'), reuse('build'), reuse('verify'), reuse('verify', 'preview'),
|
|
40
|
+
{
|
|
41
|
+
command_path: ['reports', 'save'],
|
|
42
|
+
summary: 'Build, verify and save a report into an app-owned database record.',
|
|
43
|
+
when_to_use: 'Persist an independently authored report, not an app release.',
|
|
44
|
+
args_schema: {
|
|
45
|
+
arguments: [{ token: '[dir]', key: 'dir', description: 'Report source directory.' }],
|
|
46
|
+
options: [
|
|
47
|
+
{ flags: '--database-id <id>', description: 'Required. Owning app database.' },
|
|
48
|
+
{ flags: '--document-id <id>', description: 'Existing record to update or attach to.' },
|
|
49
|
+
{ flags: '--attach', description: 'Attach to an existing non-view record.' },
|
|
50
|
+
{ flags: '--expected-revision <revision>', description: 'Fresh view revision (0 for a record without a view).' },
|
|
51
|
+
{ flags: '--title <title>', description: 'Required, including updates. Record title.' },
|
|
52
|
+
{ flags: '--context-file <file>', description: 'Required. UTF-8 readable report content and structure.' },
|
|
53
|
+
{ flags: '--properties-file <file>', description: 'JSON database property values keyed by name.' },
|
|
54
|
+
],
|
|
55
|
+
},
|
|
56
|
+
examples: ['notis reports save ./weekly-report --database-id <id> --title \"Weekly review\" --context-file ./context.md'], mutates: true, idempotent: true,
|
|
57
|
+
backend_call: { type: 'tool', name: 'LOCAL_NOTIS_SAVE_REPORT' },
|
|
58
|
+
async handler(ctx) {
|
|
59
|
+
const dir = resolveProjectDir(ctx.args.dir || '.');
|
|
60
|
+
if (!ctx.options.databaseId || !ctx.options.title || !ctx.options.contextFile) throw usageError('--database-id, --title and --context-file are required.');
|
|
61
|
+
if (ctx.options.documentId && (!/^\d+$/.test(String(ctx.options.expectedRevision ?? '')) || !Number.isSafeInteger(Number(ctx.options.expectedRevision)))) throw usageError('--expected-revision is required for update/attach.');
|
|
62
|
+
if (ctx.options.attach && !ctx.options.documentId) throw usageError('--attach requires --document-id.');
|
|
63
|
+
const context = readFileSync(resolve(ctx.options.contextFile), 'utf8');
|
|
64
|
+
const properties = ctx.options.propertiesFile ? JSON.parse(readFileSync(resolve(ctx.options.propertiesFile), 'utf8')) : {};
|
|
65
|
+
await buildArtifact(dir, { stdio: ctx.output.isMachineMode() ? 'pipe' : 'inherit' });
|
|
66
|
+
const release = prepareAppRelease(dir);
|
|
67
|
+
try {
|
|
68
|
+
if (release.manifest.routes?.length !== 1) throw usageError('Reports require exactly one SDK route.');
|
|
69
|
+
let verification;
|
|
70
|
+
const verify = appsCommandSpecs.find(item => item.command_path.join(' ') === 'apps verify');
|
|
71
|
+
const exit = await verify.handler({ ...ctx, args: { dir: release.projectDir }, options: { skipBuild: true, mode: 'stub' }, output: { ...ctx.output, isMachineMode: () => true, emitSuccess: value => { verification = value; } } });
|
|
72
|
+
if (exit !== EXIT_CODES.ok || verification?.data?.status !== 'passed') throw usageError('Report verification failed; nothing saved.');
|
|
73
|
+
const files = { ...release.files, ...Object.fromEntries(Object.entries(release.sourceFiles).map(([path, data]) => [`source/${path}`, data])) };
|
|
74
|
+
const result = await runToolCommand({ runtime: { ...ctx.runtime, timeoutMs: Math.max(ctx.runtime.timeoutMs || 0, 90000) }, toolName: 'LOCAL_NOTIS_SAVE_REPORT', mutating: true,
|
|
75
|
+
idempotencyKey: nextIdempotencyKey(ctx.globalOptions), arguments_: {
|
|
76
|
+
operation: ctx.options.attach ? 'attach' : ctx.options.documentId ? 'update' : 'create',
|
|
77
|
+
database_id: ctx.options.databaseId, title: ctx.options.title, properties,
|
|
78
|
+
...(ctx.options.documentId ? { document_id: ctx.options.documentId, expected_revision: Number(ctx.options.expectedRevision) } : {}),
|
|
79
|
+
report: { schema: 'notis-report/v2', context, artifact: { manifest: release.manifest, files, encoding: 'base64' } },
|
|
80
|
+
} });
|
|
81
|
+
if (!result?.payload?.document?.id || !result.payload.document.view_revision) throw usageError('Save returned no record identity. Read back before retrying; the outcome may be unknown.');
|
|
82
|
+
return ctx.output.emitSuccess({ command: 'reports save', data: result.payload });
|
|
83
|
+
} finally { release.close(); }
|
|
84
|
+
},
|
|
85
|
+
},
|
|
86
|
+
];
|
|
@@ -80,6 +80,8 @@ function withoutBaseSkillState(state: NotisSyncState): NotisSyncState {
|
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
export interface MaterializeCloudSkillsResult {
|
|
83
|
+
/** Cloud skills verified present in the final scoped disk scan. */
|
|
84
|
+
materializedSkillNames: string[];
|
|
83
85
|
pulled: number;
|
|
84
86
|
downloaded: number;
|
|
85
87
|
deleted: number;
|
|
@@ -89,6 +91,8 @@ export interface MaterializeCloudSkillsResult {
|
|
|
89
91
|
}
|
|
90
92
|
|
|
91
93
|
export interface MaterializeCloudSkillsOptions {
|
|
94
|
+
/** Server-verified Notis identity, when Desktop's auth subject differs. */
|
|
95
|
+
canonicalUserId?: string;
|
|
92
96
|
/** Re-link only these cloud skills without removing or adopting other links. */
|
|
93
97
|
relinkSkillNames?: readonly string[];
|
|
94
98
|
}
|
|
@@ -293,6 +297,7 @@ async function writePulledSkillsToScopedMirror(
|
|
|
293
297
|
"downloadSkillBundle" | "writeCloudSkillToDisk"
|
|
294
298
|
>,
|
|
295
299
|
failures: SkillSyncFailure[] = [],
|
|
300
|
+
writtenSkillNames: Set<string> = new Set(),
|
|
296
301
|
): Promise<number> {
|
|
297
302
|
const localSkillMap = toSkillMap(localSkills);
|
|
298
303
|
const warnSkillSync = (message: string, error: unknown): void => {
|
|
@@ -314,6 +319,7 @@ async function writePulledSkillsToScopedMirror(
|
|
|
314
319
|
})
|
|
315
320
|
) {
|
|
316
321
|
downloaded += 1;
|
|
322
|
+
writtenSkillNames.add(cloudSkill.name);
|
|
317
323
|
} else {
|
|
318
324
|
failures.push({ name: cloudSkill.name, error: "Skill content could not be downloaded or written; sync will retry" });
|
|
319
325
|
}
|
|
@@ -367,13 +373,14 @@ export async function materializeCloudSkillsForLocalShell(
|
|
|
367
373
|
);
|
|
368
374
|
}
|
|
369
375
|
|
|
370
|
-
const syncPaths = getSkillSyncPathsForUser(authUserId);
|
|
376
|
+
const syncPaths = getSkillSyncPathsForUser(options.canonicalUserId?.trim() || authUserId);
|
|
371
377
|
const pullResponse = await deps.pullSkills(serverUrl, jwt);
|
|
372
378
|
assertSkillsPullAuthorized(pullResponse);
|
|
373
379
|
const previousState = await deps.readSyncState(syncPaths);
|
|
374
380
|
|
|
375
381
|
const localSkills = await deps.scanLocalSkills(syncPaths);
|
|
376
382
|
const failedDownloads: SkillSyncFailure[] = [];
|
|
383
|
+
const writtenSkillNames = new Set<string>();
|
|
377
384
|
const downloaded = await writePulledSkillsToScopedMirror(
|
|
378
385
|
pullResponse,
|
|
379
386
|
localSkills,
|
|
@@ -381,6 +388,7 @@ export async function materializeCloudSkillsForLocalShell(
|
|
|
381
388
|
syncPaths,
|
|
382
389
|
deps,
|
|
383
390
|
failedDownloads,
|
|
391
|
+
writtenSkillNames,
|
|
384
392
|
);
|
|
385
393
|
const finalLocalSkills = await deps.scanLocalSkills(syncPaths);
|
|
386
394
|
const lastSyncedAt = pullResponse.last_synced_at || new Date().toISOString();
|
|
@@ -409,12 +417,31 @@ export async function materializeCloudSkillsForLocalShell(
|
|
|
409
417
|
}
|
|
410
418
|
for (const failure of failedDownloads) delete verifiedLinks[failure.name];
|
|
411
419
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
420
|
+
const materializedState = buildSyncState(
|
|
421
|
+
pullResponse, finalLocalSkills, lastSyncedAt, verifiedLinks,
|
|
422
|
+
new Set(failedDownloads.map(item => item.name)),
|
|
415
423
|
);
|
|
424
|
+
// Pull-only refresh is not an upload acknowledgement. Keep content baselines
|
|
425
|
+
// unless we actually wrote cloud content, and retain cloud-missing entries so
|
|
426
|
+
// the next two-way sync can delete them instead of uploading them as new.
|
|
427
|
+
for (const [name, entry] of Object.entries(materializedState.skills)) {
|
|
428
|
+
if (writtenSkillNames.has(name)) continue;
|
|
429
|
+
const previous = previousState.skills[name];
|
|
430
|
+
if (previous) {
|
|
431
|
+
entry.folderHash = previous.folderHash;
|
|
432
|
+
entry.cloudContentHash = previous.cloudContentHash;
|
|
433
|
+
} else {
|
|
434
|
+
delete materializedState.skills[name];
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
materializedState.skills = { ...previousState.skills, ...materializedState.skills };
|
|
438
|
+
await deps.writeSyncState(materializedState, syncPaths);
|
|
416
439
|
|
|
417
440
|
return {
|
|
441
|
+
materializedSkillNames: pullResponse.skills
|
|
442
|
+
.filter((skill) => finalLocalSkills.some((local) => local.name === skill.name)
|
|
443
|
+
&& !failedDownloads.some((failure) => failure.name === skill.name))
|
|
444
|
+
.map((skill) => skill.name),
|
|
418
445
|
pulled: pullResponse.skills.length,
|
|
419
446
|
downloaded,
|
|
420
447
|
deleted: 0,
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/** Serializable context shared with the agent. Apps own annotation storage. */
|
|
2
|
+
export interface ContextAttachment {
|
|
3
|
+
/** Durable HTTPS URL supplied by the app. Sent through the host media pipeline. */
|
|
4
|
+
url: string;
|
|
5
|
+
name: string;
|
|
6
|
+
mimeType: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface AgentContextContent {
|
|
10
|
+
title?: string;
|
|
11
|
+
/** Emoji, Phosphor icon name, or HTTPS image URL. Never executable markup. */
|
|
12
|
+
icon?: string;
|
|
13
|
+
/** An app-defined kind, not a closed list of annotation types. */
|
|
14
|
+
kind?: string;
|
|
15
|
+
text?: string;
|
|
16
|
+
comment?: string;
|
|
17
|
+
preview?: { format: 'text' | 'markdown'; content: string };
|
|
18
|
+
/** Any JSON-serializable value: coordinates, chart state, loaded records, etc. */
|
|
19
|
+
data?: unknown;
|
|
20
|
+
attachments?: ContextAttachment[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface AgentContextItem extends AgentContextContent {
|
|
24
|
+
/** Stable within this app. The host scopes IDs to the originating app. */
|
|
25
|
+
id: string;
|
|
26
|
+
resource?: import('./runtime').ContextResource | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Host-stamped origin. Apps describe resources; the host identifies the app/view. */
|
|
30
|
+
export interface AgentContextSource {
|
|
31
|
+
appId: string;
|
|
32
|
+
appName: string;
|
|
33
|
+
viewSlug: string;
|
|
34
|
+
viewName: string;
|
|
35
|
+
viewUrl?: string | null;
|
|
36
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import React, { useEffect, useRef, useState, type CSSProperties, type ReactNode } from 'react';
|
|
3
|
+
import type { ContextResource } from '../runtime';
|
|
4
|
+
import { NotisSelectionBoundary } from './NotisSelectionBoundary';
|
|
5
|
+
import { useAgentContext } from '../hooks/useAgentContext';
|
|
6
|
+
|
|
7
|
+
export interface NotisCommentBoxProps {
|
|
8
|
+
quote?: string;
|
|
9
|
+
value: string;
|
|
10
|
+
onChange(value: string): void;
|
|
11
|
+
onSubmit(): void;
|
|
12
|
+
onCancel(): void;
|
|
13
|
+
pending?: boolean;
|
|
14
|
+
error?: string;
|
|
15
|
+
className?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Compact, chrome-themed presentation. Apps retain ownership of comment state. */
|
|
19
|
+
export function NotisCommentBox({ quote, value, onChange, onSubmit, onCancel, pending, error, className = '' }: NotisCommentBoxProps) {
|
|
20
|
+
return <div data-notis-comment-ui role="dialog" aria-label="Comment on selection"
|
|
21
|
+
style={{ colorScheme: 'dark', background: 'hsl(var(--sidebar-background, 0 0% 7.5%))', color: 'hsl(var(--sidebar-foreground, 240 4.8% 95.9%))', borderColor: 'hsl(var(--sidebar-border, 0 0% 23.5%))' }}
|
|
22
|
+
className={`flex min-w-0 flex-col gap-2 rounded-xl border p-2.5 text-sm shadow-lg ${className}`}>
|
|
23
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
24
|
+
{quote && <blockquote title={quote} className="min-w-0 flex-1 truncate border-l-2 border-current/40 pl-2 opacity-70">{quote}</blockquote>}
|
|
25
|
+
<button type="button" aria-label="Cancel comment" onClick={onCancel} disabled={pending} className="ml-auto flex size-7 shrink-0 items-center justify-center rounded-lg opacity-70 hover:bg-white/10 hover:opacity-100 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current disabled:opacity-40 [@media(pointer:coarse)]:size-9">
|
|
26
|
+
<svg aria-hidden="true" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round"><path d="m6 6 12 12M18 6 6 18" /></svg>
|
|
27
|
+
</button>
|
|
28
|
+
</div>
|
|
29
|
+
<textarea autoFocus rows={2} name="selection-comment" aria-label="Your comment" placeholder="Add a comment…" value={value} onChange={event => onChange(event.target.value)} disabled={pending}
|
|
30
|
+
onKeyDown={event => { if (event.nativeEvent.isComposing || pending) return; if (event.key === 'Escape') { event.preventDefault(); onCancel(); } if (event.key === 'Enter' && (event.metaKey || event.ctrlKey)) { event.preventDefault(); if (value.trim()) onSubmit(); } }}
|
|
31
|
+
className="w-full min-w-0 resize-none rounded bg-transparent px-0.5 py-1 text-base text-inherit outline-none placeholder:text-current placeholder:opacity-60 sm:text-sm" />
|
|
32
|
+
{error && <p role="alert" className="text-sm text-red-300">{error}</p>}
|
|
33
|
+
<div className="flex items-center justify-between gap-3">
|
|
34
|
+
<span className="text-xs opacity-60">⌘ / Ctrl ↵ to add</span>
|
|
35
|
+
<button type="button" disabled={pending || !value.trim()} onClick={onSubmit}
|
|
36
|
+
style={{ background: 'hsl(var(--sidebar-foreground, 240 4.8% 95.9%))', color: 'hsl(var(--sidebar-background, 0 0% 7.5%))' }}
|
|
37
|
+
className="min-h-8 shrink-0 rounded-lg px-2.5 text-sm font-medium hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current disabled:opacity-40 [@media(pointer:coarse)]:min-h-9">{pending ? 'Adding…' : 'Add to chat'}</button>
|
|
38
|
+
</div>
|
|
39
|
+
</div>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface NotisCommentBoundaryProps {
|
|
43
|
+
resource?: ContextResource | null;
|
|
44
|
+
children: ReactNode;
|
|
45
|
+
className?: string;
|
|
46
|
+
commentClassName?: string;
|
|
47
|
+
/** Replace the optional standard editor while retaining selection/context behavior. */
|
|
48
|
+
renderComment?: (props: NotisCommentBoxProps) => ReactNode;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
type SelectionRect = Pick<DOMRect, 'left' | 'right' | 'top' | 'bottom'>;
|
|
52
|
+
|
|
53
|
+
/** Place the tile after the final selected line, otherwise below (or above near the viewport bottom). */
|
|
54
|
+
export function getCommentActionPosition(rects: SelectionRect[], viewport: { width: number; height: number }, rightBoundary = viewport.width, trailing: SelectionRect[] = []) {
|
|
55
|
+
const last = rects[rects.length - 1];
|
|
56
|
+
const size = 32, gap = 8;
|
|
57
|
+
const clampX = (x: number) => Math.max(gap, Math.min(viewport.width - size - gap, x));
|
|
58
|
+
const clampY = (y: number) => Math.max(gap, Math.min(viewport.height - size - gap, y));
|
|
59
|
+
const beside = { left: last.right + gap, top: clampY((last.top + last.bottom - size) / 2) };
|
|
60
|
+
const overlapsText = trailing.some(rect => rect.left < beside.left + size && rect.right > beside.left && rect.top < beside.top + size && rect.bottom > beside.top);
|
|
61
|
+
if (beside.left + size <= Math.min(viewport.width, rightBoundary) - gap && !overlapsText) return beside;
|
|
62
|
+
return { left: clampX(last.right - size), top: clampY(last.bottom + gap + size <= viewport.height - gap ? last.bottom + gap : last.top - size - gap) };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Browser paragraph selection may include an adjacent block's empty starting boundary. */
|
|
66
|
+
export function getOwnedCommentRange(selected: Range, content: Element): Range | null {
|
|
67
|
+
const owned = content.ownerDocument.createRange();
|
|
68
|
+
owned.selectNodeContents(content);
|
|
69
|
+
const range = selected.cloneRange();
|
|
70
|
+
// Only clip whitespace outside the resource; actual neighboring content must
|
|
71
|
+
// never be silently attached to this resource's comment.
|
|
72
|
+
if (range.compareBoundaryPoints(Range.START_TO_START, owned) < 0) {
|
|
73
|
+
const before = range.cloneRange();
|
|
74
|
+
before.setEnd(owned.startContainer, owned.startOffset);
|
|
75
|
+
if (before.toString().trim()) return null;
|
|
76
|
+
range.setStart(owned.startContainer, owned.startOffset);
|
|
77
|
+
}
|
|
78
|
+
if (range.compareBoundaryPoints(Range.END_TO_END, owned) > 0) {
|
|
79
|
+
const after = range.cloneRange();
|
|
80
|
+
after.setStart(owned.endContainer, owned.endOffset);
|
|
81
|
+
if (after.toString().trim()) return null;
|
|
82
|
+
range.setEnd(owned.endContainer, owned.endOffset);
|
|
83
|
+
}
|
|
84
|
+
if (range.collapsed || !content.contains(range.startContainer) || !content.contains(range.endContainer)) return null;
|
|
85
|
+
return range;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Selection-to-chat convenience. No annotation database, polling or background attachment. */
|
|
89
|
+
export function NotisCommentBoundary({ resource, children, className, commentClassName, renderComment }: NotisCommentBoundaryProps) {
|
|
90
|
+
const context = useAgentContext();
|
|
91
|
+
const root = useRef<HTMLDivElement>(null);
|
|
92
|
+
const [selection, setSelection] = useState<{ id: string; text: string; resource?: ContextResource | null; top: number; left: number } | null>(null);
|
|
93
|
+
const [editing, setEditing] = useState(false);
|
|
94
|
+
const editingRef = useRef(false);
|
|
95
|
+
const [comment, setComment] = useState('');
|
|
96
|
+
const [pending, setPending] = useState(false);
|
|
97
|
+
const [error, setError] = useState('');
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
const node = root.current;
|
|
100
|
+
if (!node) return;
|
|
101
|
+
const document = node.ownerDocument;
|
|
102
|
+
const scope = node.getRootNode() as ShadowRoot & { getSelection?: () => Selection | null };
|
|
103
|
+
const clearInvalidSelection = () => {
|
|
104
|
+
// Opening the editor focuses its input and collapses the browser range.
|
|
105
|
+
// Keep the captured quote/editor; only the unopened action follows live selection.
|
|
106
|
+
if (editingRef.current) return;
|
|
107
|
+
const selected = scope.getSelection?.() || document.defaultView?.getSelection();
|
|
108
|
+
const content = node.firstElementChild;
|
|
109
|
+
if (!selected?.rangeCount || selected.isCollapsed || !selected.toString().trim()
|
|
110
|
+
|| !content || !getOwnedCommentRange(selected.getRangeAt(0), content)) setSelection(null);
|
|
111
|
+
};
|
|
112
|
+
document.addEventListener('selectionchange', clearInvalidSelection);
|
|
113
|
+
if (scope !== (document as unknown as ShadowRoot)) scope.addEventListener('selectionchange', clearInvalidSelection);
|
|
114
|
+
return () => {
|
|
115
|
+
document.removeEventListener('selectionchange', clearInvalidSelection);
|
|
116
|
+
if (scope !== (document as unknown as ShadowRoot)) scope.removeEventListener('selectionchange', clearInvalidSelection);
|
|
117
|
+
};
|
|
118
|
+
}, []);
|
|
119
|
+
const open = () => { editingRef.current = true; setEditing(true); };
|
|
120
|
+
const capture = (target: EventTarget | null) => {
|
|
121
|
+
// Clicking the action/editor must not unmount it before its click runs.
|
|
122
|
+
if (editing || (target instanceof Element && target.closest('[data-notis-comment-ui]'))) return;
|
|
123
|
+
const node = root.current;
|
|
124
|
+
const scope = node?.getRootNode() as ShadowRoot & { getSelection?: () => Selection | null };
|
|
125
|
+
const selected = scope?.getSelection?.() || window.getSelection();
|
|
126
|
+
if (!node || !selected?.rangeCount || selected.isCollapsed) { setSelection(null); return; }
|
|
127
|
+
const content = node.firstElementChild;
|
|
128
|
+
const range = content ? getOwnedCommentRange(selected.getRangeAt(0), content) : null;
|
|
129
|
+
if (!range) { setSelection(null); return; }
|
|
130
|
+
const start = range.startContainer.nodeType === Node.ELEMENT_NODE ? range.startContainer as Element : range.startContainer.parentElement;
|
|
131
|
+
const end = range.endContainer.nodeType === Node.ELEMENT_NODE ? range.endContainer as Element : range.endContainer.parentElement;
|
|
132
|
+
if (!node.contains(range.startContainer) || !node.contains(range.endContainer)
|
|
133
|
+
|| start?.closest('input,textarea,button,[contenteditable],[data-notis-comment-ui]')
|
|
134
|
+
|| end?.closest('input,textarea,button,[contenteditable],[data-notis-comment-ui]')) { setSelection(null); return; }
|
|
135
|
+
const rect = range.getBoundingClientRect();
|
|
136
|
+
const lines = Array.from(range.getClientRects?.() || []).filter(line => line.width > 0 && line.height > 0);
|
|
137
|
+
const boundary = node.firstElementChild?.getBoundingClientRect();
|
|
138
|
+
const trailing: DOMRect[] = [];
|
|
139
|
+
const endBlock = end?.closest('p,li,h1,h2,h3,h4,h5,h6,td,th,div');
|
|
140
|
+
if (endBlock && node.contains(endBlock)) {
|
|
141
|
+
const remainder = range.cloneRange();
|
|
142
|
+
remainder.selectNodeContents(endBlock);
|
|
143
|
+
remainder.setStart(range.endContainer, range.endOffset);
|
|
144
|
+
trailing.push(...Array.from(remainder.getClientRects?.() || []).filter(line => line.width > 0 && line.height > 0));
|
|
145
|
+
}
|
|
146
|
+
const position = getCommentActionPosition(lines.length ? lines : [rect], { width: window.innerWidth, height: window.innerHeight }, boundary?.width ? boundary.right : window.innerWidth, trailing);
|
|
147
|
+
setSelection({ id: crypto.randomUUID(), text: selected.toString().trim(), resource: resource ? JSON.parse(JSON.stringify(resource)) : null, ...position });
|
|
148
|
+
};
|
|
149
|
+
const close = () => { editingRef.current = false; setEditing(false); setSelection(null); setComment(''); setError(''); };
|
|
150
|
+
const submit = async () => {
|
|
151
|
+
if (!selection || !comment.trim() || pending) return;
|
|
152
|
+
setPending(true); setError('');
|
|
153
|
+
try {
|
|
154
|
+
const added = await context.add({ id: selection.id, kind: 'comment', title: selection.resource?.label || 'Comment', icon: 'phosphor:chat-text', text: selection.text, comment, resource: selection.resource });
|
|
155
|
+
if (!added) throw new Error('The comment could not be added to chat.');
|
|
156
|
+
close();
|
|
157
|
+
} catch (reason) { setError(reason instanceof Error ? reason.message : 'Could not add this comment.'); }
|
|
158
|
+
finally { setPending(false); }
|
|
159
|
+
};
|
|
160
|
+
const editorProps: NotisCommentBoxProps = { quote: selection?.text, value: comment, onChange: setComment, onSubmit: () => void submit(), onCancel: close, pending, error, className: commentClassName };
|
|
161
|
+
return <div ref={root} className="contents" onMouseUp={event => capture(event.target)} onDoubleClick={event => capture(event.target)} onKeyUp={event => { if (!editing && (event.shiftKey || event.key === 'Shift' || ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'a'))) capture(event.target); }}>
|
|
162
|
+
<NotisSelectionBoundary resource={resource} className={className} style={className ? undefined : { display: 'contents' }}>{children}</NotisSelectionBoundary>
|
|
163
|
+
{selection && <div data-notis-comment-ui className="fixed z-50 max-h-[calc(100dvh-1rem)]" style={{ overflow: editing ? 'auto' : 'visible', top: editing ? Math.min(selection.top, Math.max(8, window.innerHeight - 320)) : selection.top, left: `clamp(8px, ${selection.left}px, calc(100vw - ${editing ? 'min(20rem, calc(100vw - 1rem))' : '2rem'} - 8px))`, width: editing ? 'min(20rem, calc(100vw - 1rem))' : undefined } as CSSProperties}>
|
|
164
|
+
{editing ? (renderComment ? renderComment(editorProps) : <NotisCommentBox {...editorProps} />) : <button type="button" aria-label="Comment" title="Comment" onMouseDown={event => event.preventDefault()} onClick={open}
|
|
165
|
+
style={{ background: 'hsl(var(--sidebar-background, 0 0% 7.5%))', color: 'hsl(var(--sidebar-foreground, 240 4.8% 95.9%))' }}
|
|
166
|
+
className="relative flex size-8 items-center justify-center rounded-lg ring-1 ring-white/15 hover:opacity-90 focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current">
|
|
167
|
+
<svg aria-hidden="true" width="18" height="18" viewBox="0 0 256 256" fill="currentColor"><path d="M216,48H40A16,16,0,0,0,24,64V224a15.85,15.85,0,0,0,9.24,14.5A16.13,16.13,0,0,0,40,240a15.89,15.89,0,0,0,10.25-3.78l.09-.07L83,208H216a16,16,0,0,0,16-16V64A16,16,0,0,0,216,48ZM40,224h0ZM216,192H80a8,8,0,0,0-5.23,1.95L40,224V64H216ZM88,112a8,8,0,0,1,8-8h64a8,8,0,0,1,0,16H96A8,8,0,0,1,88,112Zm0,32a8,8,0,0,1,8-8h64a8,8,0,1,1,0,16H96A8,8,0,0,1,88,144Z" /></svg>
|
|
168
|
+
<span aria-hidden="true" className="absolute left-1/2 top-1/2 hidden size-12 -translate-x-1/2 -translate-y-1/2 [@media(pointer:coarse)]:block" />
|
|
169
|
+
</button>}
|
|
170
|
+
</div>}
|
|
171
|
+
</div>;
|
|
172
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { CSSProperties, ClipboardEvent, ReactNode } from 'react';
|
|
2
|
-
import { useCallback } from 'react';
|
|
2
|
+
import React, { useCallback } from 'react';
|
|
3
3
|
import { useNotisRuntime } from '../provider';
|
|
4
4
|
import type { ContextResource, ContextSelection } from '../runtime';
|
|
5
5
|
|
|
@@ -31,10 +31,13 @@ export function NotisSelectionBoundary({
|
|
|
31
31
|
}: NotisSelectionBoundaryProps) {
|
|
32
32
|
const runtime = useNotisRuntime();
|
|
33
33
|
const onCopy = useCallback((event: ClipboardEvent<HTMLDivElement>) => {
|
|
34
|
-
const
|
|
35
|
-
|
|
34
|
+
const target = event.target as Element | null;
|
|
35
|
+
if (target?.closest?.('input,textarea,[contenteditable="true"]')) return;
|
|
36
|
+
const scope = event.currentTarget.getRootNode() as ShadowRoot & { getSelection?: () => Selection | null };
|
|
37
|
+
const selection = scope?.getSelection?.() || window.getSelection();
|
|
38
|
+
const text = selection?.toString() ?? '';
|
|
36
39
|
if (
|
|
37
|
-
!text
|
|
40
|
+
!text.trim()
|
|
38
41
|
|| !selection
|
|
39
42
|
|| !event.currentTarget.contains(selection.anchorNode)
|
|
40
43
|
|| !event.currentTarget.contains(selection.focusNode)
|
|
@@ -42,7 +45,8 @@ export function NotisSelectionBoundary({
|
|
|
42
45
|
|
|
43
46
|
const payload: ContextSelection = {
|
|
44
47
|
id: selectionId(),
|
|
45
|
-
text
|
|
48
|
+
text,
|
|
49
|
+
...(runtime?.contextSource ? { source: runtime.contextSource } : {}),
|
|
46
50
|
...(resource ? { resource: { ...resource, snapshot: undefined } } : {}),
|
|
47
51
|
};
|
|
48
52
|
event.clipboardData.setData('text/plain', text);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
import { useMemo } from 'react';
|
|
3
|
+
import { useNotisRuntime } from '../provider';
|
|
4
|
+
import type { AgentContextItem } from '../agentContext';
|
|
5
|
+
|
|
6
|
+
/** Share context without owning storage, opening a new thread, or sending a message. */
|
|
7
|
+
export function useAgentContext() {
|
|
8
|
+
const runtime = useNotisRuntime();
|
|
9
|
+
return useMemo(() => ({
|
|
10
|
+
add(item: AgentContextItem): Promise<boolean> {
|
|
11
|
+
if (!runtime?.addContext) return Promise.reject(new Error('Chat context is unavailable in this host.'));
|
|
12
|
+
return runtime.addContext(item);
|
|
13
|
+
},
|
|
14
|
+
update(item: AgentContextItem): Promise<boolean> {
|
|
15
|
+
if (!runtime?.updateContext) return Promise.reject(new Error('Chat context is unavailable in this host.'));
|
|
16
|
+
return runtime.updateContext(item);
|
|
17
|
+
},
|
|
18
|
+
remove(id: string): Promise<boolean> {
|
|
19
|
+
if (!runtime?.removeContext) return Promise.reject(new Error('Chat context is unavailable in this host.'));
|
|
20
|
+
return runtime.removeContext(id);
|
|
21
|
+
},
|
|
22
|
+
}), [runtime]);
|
|
23
|
+
}
|
|
@@ -154,3 +154,8 @@ export { Skeleton, ViewSkeleton } from './components/Skeleton';
|
|
|
154
154
|
|
|
155
155
|
export { useLongPressSelection } from './hooks/useLongPressSelection';
|
|
156
156
|
export { isInteractionElementVisible } from './interactions/visibility';
|
|
157
|
+
|
|
158
|
+
export type { AgentContextContent, AgentContextItem, ContextAttachment } from './agentContext';
|
|
159
|
+
export { useAgentContext } from './hooks/useAgentContext';
|
|
160
|
+
export { NotisCommentBoundary, NotisCommentBox } from './components/NotisCommentBoundary';
|
|
161
|
+
export type { NotisCommentBoundaryProps, NotisCommentBoxProps } from './components/NotisCommentBoundary';
|