@hosty-sdk/app 0.35.1 → 0.38.0

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 CHANGED
@@ -298,6 +298,30 @@ PCM16 WAV. Browser microphone consent is separate from Core permission.
298
298
  credentials and the acting user's access. It does not expose general agent execution.
299
299
  See [Provider consumption](../../docs/features/provider-consumption/feature.md) for the full contract.
300
300
 
301
+ ## Tasks And Execution Providers
302
+
303
+ Core 0.159.0 and SDK 0.36.0 support independent `tasks` and `task-executor` categories. Request
304
+ `providers.tasks` or `providers.task-executor`, select an app/installation/key explicitly and use
305
+ server-only clients. Models, credentials and Harness profile internals are not shared task fields.
306
+
307
+ ```ts
308
+ import { ProviderClient } from "@hosty-sdk/app/providers/server";
309
+ import { TaskClient, TaskExecutorClient } from "@hosty-sdk/app/tasks/server";
310
+ const providers = new ProviderClient();
311
+ const tasks = new TaskClient(providers, selectedTaskProvider, getCurrentUserToken);
312
+ const draft = await tasks.create({ requestId, input: {
313
+ title: "Summarize the meeting", markdown: "List decisions and next steps.",
314
+ assignee: { kind: "app", executor: selectedExecutor, optionId }, resources: [],
315
+ } });
316
+ await tasks.action(draft.ref, { requestId: readyRequestId, expectedRevision: draft.revision, action: "ready" });
317
+ const executions = new TaskExecutorClient(providers, selectedExecutor, getCurrentUserToken);
318
+ const receipt = await executions.get(executionId);
319
+ ```
320
+
321
+ Retain each UUID and exact body after a timeout. Clients do not retry mutations implicitly. A task
322
+ provider persists execution intent before PUT; completed model output alone is not a task result.
323
+ See [Basic Tasks](../../docs/features/basic-tasks/feature.md) for the protocol and authority boundaries.
324
+
301
325
  ### Assistant MCP credentials
302
326
 
303
327
  MCP handlers use `introspectMcpToken` from `@hosty-sdk/app/scoped-token` for online validation of
@@ -568,16 +592,49 @@ navigation/launcher and disabled tools until they provide a panel host. `panelLa
568
592
  rendering choice, not a disable policy: explicitly disable local tools or register a renderer.
569
593
  See [UI composition and browser acceptance](../../docs/features/sdk-ui-composition/feature.md).
570
594
 
595
+ ## Annotated feedback (Core 0.165.0 / SDK 0.38.0)
596
+
597
+ `HostyUiRoot` includes an authenticated Annotate entry through the existing UI server adapter.
598
+ Users can select an element/region or add a page comment, enter text beside it, edit that same note
599
+ in the side panel and submit several annotations as one report. IndexedDB preserves text and image
600
+ blobs by Core/app/installation/user. Storage failure is visible; confirmed submission clears the
601
+ draft, while an uncertain response retains its exact UUID for retry.
602
+
603
+ Screenshots use the standard Screen Capture API chooser, still-image preview/crop and explicit
604
+ inclusion. Shared SDK frames delegate `display-capture` to their source; browser/webview support and
605
+ parent policy still apply. Text feedback works when capture is unsupported or denied. Optional
606
+ dictation requires an approved `providers.speech-to-text` permission and discovered provider.
607
+
608
+ Own-app intake needs app access, not a management grant. Administrator clients can use
609
+ `FeedbackServerClient` from `@hosty-sdk/app/feedback/server` with the reviewed `apps.feedback.manage`
610
+ permission and the current app-user token; DTOs are exported from `@hosty-sdk/app/feedback`.
611
+ These server methods expose fixed Core contracts, never a browser-accessible arbitrary proxy.
612
+
613
+ A feedback-review client can mark its declared navigation item with `feedbackReview: true`. Core
614
+ resolves its own endpoint/path through normal app launch with a `feedback=<report-id>` query. The
615
+ client opens a review, not an automatic dispatch. The declaration grants no authority. Ordinary
616
+ reporters get immediate submission confirmation but no processing inbox or notifications.
617
+
618
+ See [App Feedback Inbox](../../docs/features/app-feedback-inbox/feature.md) for limits, access,
619
+ manual chat dispatch, exact-prompt outcomes and workspace deletion retention.
620
+
571
621
  ## Child Core sandbox controls
572
622
 
573
623
  `@hosty-sdk/app/sandbox/react` exports `CoreEnvironmentPanel` for operator-facing child Core
574
624
  management. Supply an authenticated `EnvironmentRequest` transport, a tab-local replay storage key
575
- and optionally a workspace with its active worktrees. Shell and Workspaces use this shared panel
576
- for create/start, current/source Core selection, normal test-account setup, app status, preview and
577
- confirmed deletion. The component does not grant authority: Core independently checks the optional
625
+ and optionally a workspace with its active worktrees. It supports create/start, current/source
626
+ Core selection, normal test-account setup, app status, preview and confirmed deletion. Shell has
627
+ its own combined Core/environment rows and current-mode creation dialog. The component does not grant authority: Core independently checks the optional
578
628
  `core.environments.inspect/operate` grants, administrator identity and workspace source access.
579
629
 
580
- `@hosty-sdk/app/sandbox/server` provides the server-side clients for independent app sandboxes and
581
- child Core environments. Caller-owned `core.environments.read/manage` authority remains distinct
630
+ `@hosty-sdk/app/sandbox/server` provides CoreEnvironmentClient for child Core environments. Caller-owned `core.environments.read/manage` authority remains distinct
582
631
  from the operator panel's cross-caller permissions. See the
583
- [sandbox contract](../../docs/features/app-sandbox-runtimes/feature.md) for lifecycle and cleanup rules.
632
+ [environment contract](../../docs/features/app-sandbox-runtimes/feature.md) for lifecycle and cleanup rules.
633
+
634
+ Version 0.37 removes SandboxClient, SandboxAction, the single-app Sandbox* request/view types,
635
+ SandboxIdentity/SandboxTarget, readSandboxIdentity and readSandboxTarget. No relay identity or
636
+ HOSTY_SANDBOX_* protocol is supported. Use CoreEnvironmentClient plus ordinary child app/user APIs;
637
+ apps run with the same normal identity and app-owned roles as any other Core installation. Existing
638
+ core.environments.* grants require their ordinary review; removed apps.sandboxes.* grants confer
639
+ no replacement authority. The /sandbox/server and /sandbox/react module paths remain for the
640
+ retained child-environment API.
@@ -0,0 +1,3 @@
1
+ /** Interoperable speech v1 baseline: mono PCM16 WAV at 16 kHz. */
2
+ export declare function pcmWave(samples: Float32Array): Blob;
3
+ export declare function recordingWave(recording: Blob): Promise<Blob>;
@@ -0,0 +1,40 @@
1
+ /** Interoperable speech v1 baseline: mono PCM16 WAV at 16 kHz. */
2
+ export function pcmWave(samples) {
3
+ if (!samples.length || samples.length > 120 * 16000)
4
+ throw new Error("Record between a moment and 120 seconds of audio.");
5
+ const bytes = new ArrayBuffer(44 + samples.length * 2), view = new DataView(bytes);
6
+ const text = (at, value) => { for (let i = 0; i < value.length; i++)
7
+ view.setUint8(at + i, value.charCodeAt(i)); };
8
+ text(0, "RIFF");
9
+ view.setUint32(4, bytes.byteLength - 8, true);
10
+ text(8, "WAVE");
11
+ text(12, "fmt ");
12
+ view.setUint32(16, 16, true);
13
+ view.setUint16(20, 1, true);
14
+ view.setUint16(22, 1, true);
15
+ view.setUint32(24, 16000, true);
16
+ view.setUint32(28, 32000, true);
17
+ view.setUint16(32, 2, true);
18
+ view.setUint16(34, 16, true);
19
+ text(36, "data");
20
+ view.setUint32(40, samples.length * 2, true);
21
+ samples.forEach((sample, i) => { const value = Math.max(-1, Math.min(1, sample)); view.setInt16(44 + i * 2, Math.round(value * (value < 0 ? 32768 : 32767)), true); });
22
+ return new Blob([bytes], { type: "audio/wav" });
23
+ }
24
+ export async function recordingWave(recording) {
25
+ const context = new AudioContext();
26
+ try {
27
+ const decoded = await context.decodeAudioData(await recording.arrayBuffer());
28
+ if (decoded.duration > 120.1)
29
+ throw new Error("Recordings may be up to 120 seconds.");
30
+ const offline = new OfflineAudioContext(1, Math.min(120 * 16000, Math.ceil(decoded.duration * 16000)), 16000);
31
+ const source = offline.createBufferSource();
32
+ source.buffer = decoded;
33
+ source.connect(offline.destination);
34
+ source.start();
35
+ return pcmWave((await offline.startRendering()).getChannelData(0));
36
+ }
37
+ finally {
38
+ await context.close();
39
+ }
40
+ }
@@ -0,0 +1,3 @@
1
+ import type { FeedbackRect } from "./feedback-types.js";
2
+ export declare function captureStill(hide: (hidden: boolean) => void, signal: AbortSignal): Promise<Blob>;
3
+ export declare function cropStill(blob: Blob, crop: FeedbackRect | null): Promise<Blob>;
@@ -0,0 +1,80 @@
1
+ export async function captureStill(hide, signal) {
2
+ if (!navigator.mediaDevices?.getDisplayMedia)
3
+ throw new Error("Screen capture is unavailable in this browser. You can still send comments.");
4
+ // Offer the application tab while retaining the browser-owned chooser and permission.
5
+ const options = {
6
+ video: true, audio: false, preferCurrentTab: true, selfBrowserSurface: "include",
7
+ };
8
+ const stream = await navigator.mediaDevices.getDisplayMedia(options);
9
+ const video = document.createElement("video");
10
+ const stop = () => { stream.getTracks().forEach(t => t.stop()); };
11
+ signal.addEventListener("abort", stop, { once: true });
12
+ try {
13
+ signal.throwIfAborted();
14
+ hide(true);
15
+ // Let the hidden overlay reach the compositor before consuming the live stream.
16
+ await new Promise(resolve => setTimeout(resolve, 100));
17
+ signal.throwIfAborted();
18
+ video.muted = true;
19
+ video.playsInline = true;
20
+ await new Promise((resolve, reject) => {
21
+ let frame, fallback;
22
+ const cleanup = () => {
23
+ clearTimeout(timeout);
24
+ clearTimeout(fallback);
25
+ signal.removeEventListener("abort", abort);
26
+ if (frame !== undefined)
27
+ video.cancelVideoFrameCallback(frame);
28
+ };
29
+ const fail = (error) => { cleanup(); reject(error); };
30
+ const abort = () => fail(signal.reason);
31
+ const ready = () => { cleanup(); resolve(); };
32
+ const timeout = setTimeout(() => fail(new Error("The capture source did not produce a frame.")), 5000);
33
+ signal.addEventListener("abort", abort, { once: true });
34
+ // Register before play: static tab capture may deliver only one frame until pixels change.
35
+ if ("requestVideoFrameCallback" in video)
36
+ frame = video.requestVideoFrameCallback(ready);
37
+ video.srcObject = stream;
38
+ void video.play().then(() => {
39
+ if (!("requestVideoFrameCallback" in video))
40
+ fallback = setTimeout(ready, 150);
41
+ }, fail);
42
+ });
43
+ signal.throwIfAborted();
44
+ if (!video.videoWidth || !video.videoHeight)
45
+ throw new Error("The selected capture surface has no image.");
46
+ const scale = Math.min(1, 2560 / Math.max(video.videoWidth, video.videoHeight));
47
+ const canvas = document.createElement("canvas");
48
+ canvas.width = Math.round(video.videoWidth * scale);
49
+ canvas.height = Math.round(video.videoHeight * scale);
50
+ canvas.getContext("2d").drawImage(video, 0, 0, canvas.width, canvas.height);
51
+ return await canvasBlob(canvas);
52
+ }
53
+ finally {
54
+ stop();
55
+ signal.removeEventListener("abort", stop);
56
+ video.srcObject = null;
57
+ hide(false);
58
+ }
59
+ }
60
+ export async function cropStill(blob, crop) {
61
+ if (!crop)
62
+ return blob;
63
+ const image = await createImageBitmap(blob);
64
+ try {
65
+ const x = Math.max(0, Math.min(image.width - 1, Math.floor(crop.x * image.width))), y = Math.max(0, Math.min(image.height - 1, Math.floor(crop.y * image.height)));
66
+ const width = Math.max(1, Math.min(image.width - x, Math.round(crop.width * image.width)));
67
+ const height = Math.max(1, Math.min(image.height - y, Math.round(crop.height * image.height)));
68
+ const canvas = document.createElement("canvas");
69
+ canvas.width = width;
70
+ canvas.height = height;
71
+ canvas.getContext("2d").drawImage(image, x, y, width, height, 0, 0, width, height);
72
+ return await canvasBlob(canvas);
73
+ }
74
+ finally {
75
+ image.close();
76
+ }
77
+ }
78
+ function canvasBlob(canvas) {
79
+ return new Promise((resolve, reject) => canvas.toBlob(blob => blob ? resolve(blob) : reject(new Error("Could not encode the screenshot.")), "image/png"));
80
+ }
@@ -0,0 +1,20 @@
1
+ import type { FeedbackCapabilities, FeedbackNoteInput, FeedbackSubmit, FeedbackTarget } from "./feedback-types.js";
2
+ export type DraftNote = Omit<FeedbackNoteInput, "image"> & {
3
+ image?: Blob;
4
+ };
5
+ export type FeedbackDraft = {
6
+ key: string;
7
+ revision: number;
8
+ requestId: string;
9
+ comment: string;
10
+ annotations: DraftNote[];
11
+ submitting: boolean;
12
+ };
13
+ export declare function draftKey(capabilities: FeedbackCapabilities): string;
14
+ export declare function readDraft(key: string): Promise<FeedbackDraft | null>;
15
+ /** Read-check-write is one transaction: an older tab cannot overwrite a newer draft. */
16
+ export declare function saveDraft(draft: FeedbackDraft, expectedRevision: number, expectedRequestId: string | null): Promise<void>;
17
+ export declare function deleteDraft(key: string, requestId: string, expectedRevision: number): Promise<void>;
18
+ export declare function blankDraft(key: string): FeedbackDraft;
19
+ export declare function makeNote(target: FeedbackTarget): DraftNote;
20
+ export declare function submission(draft: FeedbackDraft): Promise<FeedbackSubmit>;
@@ -0,0 +1,104 @@
1
+ const database = "hosty-feedback-v1";
2
+ export function draftKey(capabilities) {
3
+ return JSON.stringify([capabilities.coreId, capabilities.appId, capabilities.installation, capabilities.userId]);
4
+ }
5
+ function open() {
6
+ return new Promise((resolve, reject) => {
7
+ if (!globalThis.indexedDB) {
8
+ reject(new Error("Local draft storage is unavailable."));
9
+ return;
10
+ }
11
+ const request = indexedDB.open(database, 1);
12
+ request.onupgradeneeded = () => { request.result.createObjectStore("drafts", { keyPath: "key" }); };
13
+ request.onsuccess = () => resolve(request.result);
14
+ request.onerror = () => reject(request.error ?? new Error("Could not open draft storage."));
15
+ request.onblocked = () => reject(new Error("Close older feedback tabs to update draft storage."));
16
+ });
17
+ }
18
+ export async function readDraft(key) {
19
+ const db = await open();
20
+ try {
21
+ return await new Promise((resolve, reject) => {
22
+ const tx = db.transaction("drafts", "readonly");
23
+ const request = tx.objectStore("drafts").get(key);
24
+ let value = null;
25
+ request.onsuccess = () => { value = request.result ?? null; };
26
+ tx.oncomplete = () => resolve(value);
27
+ tx.onerror = () => reject(tx.error);
28
+ tx.onabort = () => reject(tx.error);
29
+ });
30
+ }
31
+ finally {
32
+ db.close();
33
+ }
34
+ }
35
+ /** Read-check-write is one transaction: an older tab cannot overwrite a newer draft. */
36
+ export async function saveDraft(draft, expectedRevision, expectedRequestId) {
37
+ const db = await open();
38
+ try {
39
+ await new Promise((resolve, reject) => {
40
+ const tx = db.transaction("drafts", "readwrite");
41
+ const store = tx.objectStore("drafts");
42
+ const read = store.get(draft.key);
43
+ let conflict = false;
44
+ read.onsuccess = () => {
45
+ if ((read.result?.revision ?? 0) !== expectedRevision || (read.result?.requestId ?? null) !== expectedRequestId) {
46
+ conflict = true;
47
+ tx.abort();
48
+ return;
49
+ }
50
+ store.put(draft);
51
+ };
52
+ tx.oncomplete = () => resolve();
53
+ tx.onabort = tx.onerror = () => reject(conflict ? new Error("This draft changed in another tab. Reload to restore its latest saved version.") : tx.error ?? new Error("Draft could not be saved locally."));
54
+ });
55
+ }
56
+ finally {
57
+ db.close();
58
+ }
59
+ }
60
+ export async function deleteDraft(key, requestId, expectedRevision) {
61
+ const db = await open();
62
+ try {
63
+ await new Promise((resolve, reject) => {
64
+ const tx = db.transaction("drafts", "readwrite");
65
+ const store = tx.objectStore("drafts");
66
+ const read = store.get(key);
67
+ let conflict = false;
68
+ read.onsuccess = () => {
69
+ if (!read.result)
70
+ return;
71
+ if (read.result.requestId !== requestId || read.result.revision !== expectedRevision) {
72
+ conflict = true;
73
+ tx.abort();
74
+ return;
75
+ }
76
+ store.delete(key);
77
+ };
78
+ tx.oncomplete = () => resolve();
79
+ tx.onabort = tx.onerror = () => reject(conflict ? new Error("This draft changed in another tab. Reload before discarding its saved content.") : tx.error ?? new Error("Draft could not be removed locally."));
80
+ });
81
+ }
82
+ finally {
83
+ db.close();
84
+ }
85
+ }
86
+ export function blankDraft(key) {
87
+ return { key, revision: 0, requestId: crypto.randomUUID(), comment: "", annotations: [], submitting: false };
88
+ }
89
+ export function makeNote(target) {
90
+ return { id: crypto.randomUUID(), text: "", route: location.pathname, capturedAt: new Date().toISOString(), target };
91
+ }
92
+ export async function submission(draft) {
93
+ const [coreId, appId, installation, userId] = JSON.parse(draft.key);
94
+ return { scope: { coreId: coreId, appId: appId, installation: installation, userId: userId }, requestId: draft.requestId, comment: draft.comment, annotations: await Promise.all(draft.annotations.map(async (note) => {
95
+ const { image, ...data } = note;
96
+ if (!image)
97
+ return data;
98
+ const bytes = new Uint8Array(await image.arrayBuffer());
99
+ let binary = "";
100
+ for (let i = 0; i < bytes.length; i += 8192)
101
+ binary += String.fromCharCode(...bytes.subarray(i, i + 8192));
102
+ return { ...data, image: { mediaType: image.type, data: btoa(binary) } };
103
+ })) };
104
+ }
@@ -0,0 +1,4 @@
1
+ export declare function FeedbackCapture({ active, uiPath }: {
2
+ active: boolean;
3
+ uiPath?: string;
4
+ }): import("react").JSX.Element | null;