@hosty-sdk/app 0.37.0 → 0.39.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 +26 -0
- package/dist/agent-sessions.d.ts +73 -0
- package/dist/agent-sessions.js +1 -0
- package/dist/feedback-audio.d.ts +3 -0
- package/dist/feedback-audio.js +40 -0
- package/dist/feedback-capture.d.ts +3 -0
- package/dist/feedback-capture.js +80 -0
- package/dist/feedback-draft.d.ts +20 -0
- package/dist/feedback-draft.js +104 -0
- package/dist/feedback-react.d.ts +4 -0
- package/dist/feedback-react.js +390 -0
- package/dist/feedback-server.d.ts +31 -0
- package/dist/feedback-server.js +48 -0
- package/dist/feedback-speech.d.ts +6 -0
- package/dist/feedback-speech.js +119 -0
- package/dist/feedback-types.d.ts +122 -0
- package/dist/feedback-types.js +9 -0
- package/dist/history.d.ts +354 -0
- package/dist/history.js +1 -0
- package/dist/providers-server.d.ts +17 -1
- package/dist/providers-server.js +33 -1
- package/dist/providers.d.ts +76 -1
- package/dist/tasks-server.d.ts +11 -0
- package/dist/tasks-server.js +15 -0
- package/dist/ui-frame.js +1 -1
- package/dist/ui-react.js +2 -1
- package/dist/ui-server.js +39 -4
- package/package.json +17 -1
package/README.md
CHANGED
|
@@ -592,6 +592,32 @@ navigation/launcher and disabled tools until they provide a panel host. `panelLa
|
|
|
592
592
|
rendering choice, not a disable policy: explicitly disable local tools or register a renderer.
|
|
593
593
|
See [UI composition and browser acceptance](../../docs/features/sdk-ui-composition/feature.md).
|
|
594
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
|
+
|
|
595
621
|
## Child Core sandbox controls
|
|
596
622
|
|
|
597
623
|
`@hosty-sdk/app/sandbox/react` exports `CoreEnvironmentPanel` for operator-facing child Core
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/** Local-to-Core identities. Never infer an app, conversation, native session or run from another ID. */
|
|
2
|
+
export type ProviderRef = {
|
|
3
|
+
appId: string;
|
|
4
|
+
installationId: string;
|
|
5
|
+
key: string;
|
|
6
|
+
};
|
|
7
|
+
export type ConversationRef = {
|
|
8
|
+
provider: ProviderRef;
|
|
9
|
+
id: string;
|
|
10
|
+
};
|
|
11
|
+
export type NativeSessionRef = {
|
|
12
|
+
provider: ProviderRef;
|
|
13
|
+
id: string;
|
|
14
|
+
};
|
|
15
|
+
export type RunRef = {
|
|
16
|
+
provider: ProviderRef;
|
|
17
|
+
id: string;
|
|
18
|
+
};
|
|
19
|
+
/** Decimal strings avoid integer truncation across language boundaries. */
|
|
20
|
+
export type Revision = string;
|
|
21
|
+
export type AgentSession = {
|
|
22
|
+
ref: NativeSessionRef;
|
|
23
|
+
revision: Revision;
|
|
24
|
+
activeRuns: RunRef[];
|
|
25
|
+
capabilities: string[];
|
|
26
|
+
conversation?: ConversationRef | null;
|
|
27
|
+
};
|
|
28
|
+
export type AgentRun = {
|
|
29
|
+
ref: RunRef;
|
|
30
|
+
revision: Revision;
|
|
31
|
+
state: "queued" | "running" | "waiting_for_input" | "succeeded" | "failed" | "cancelled" | "unknown";
|
|
32
|
+
observedAt: string;
|
|
33
|
+
freshness: "current" | "last_known" | "unavailable";
|
|
34
|
+
stopConfirmed: boolean;
|
|
35
|
+
nativeSession?: NativeSessionRef | null;
|
|
36
|
+
conversation?: ConversationRef | null;
|
|
37
|
+
};
|
|
38
|
+
export type AgentSessionPage = {
|
|
39
|
+
sessions: AgentSession[];
|
|
40
|
+
nextCursor: string | null;
|
|
41
|
+
availability: "available" | "partial" | "unavailable";
|
|
42
|
+
observedAt: string;
|
|
43
|
+
};
|
|
44
|
+
export type AgentSessionQuery = {
|
|
45
|
+
provider: ProviderRef;
|
|
46
|
+
conversation?: ConversationRef;
|
|
47
|
+
cursor?: string;
|
|
48
|
+
limit?: number;
|
|
49
|
+
};
|
|
50
|
+
export type RunCancel = {
|
|
51
|
+
run: RunRef;
|
|
52
|
+
requestId: string;
|
|
53
|
+
expectedRevision: Revision;
|
|
54
|
+
};
|
|
55
|
+
export type RunCancelReceipt = {
|
|
56
|
+
requestId: string;
|
|
57
|
+
state: "accepted" | "completed";
|
|
58
|
+
run: AgentRun;
|
|
59
|
+
};
|
|
60
|
+
export type AgentRunNotification = {
|
|
61
|
+
run: AgentRun;
|
|
62
|
+
cursor: string;
|
|
63
|
+
rescanRequired: boolean;
|
|
64
|
+
};
|
|
65
|
+
export type AgentRunSubscription = {
|
|
66
|
+
handle: string;
|
|
67
|
+
expiresAt: string;
|
|
68
|
+
initial: AgentRunNotification;
|
|
69
|
+
};
|
|
70
|
+
export type AgentRunSubscriptionCreate = {
|
|
71
|
+
run: RunRef;
|
|
72
|
+
cursor?: string;
|
|
73
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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,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
|
+
}
|