@hosty-sdk/app 0.34.2 → 0.35.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.
@@ -0,0 +1,35 @@
1
+ import type { ProviderServerOptions } from "./providers-server.js";
2
+ import type { ToolActivity, ToolActivityState } from "./ui-types.js";
3
+ export type ToolActivityValue = {
4
+ state: ToolActivityState;
5
+ count?: number;
6
+ label?: string;
7
+ ttlSeconds?: number;
8
+ };
9
+ export type ToolActivityReporterOptions = ProviderServerOptions & {
10
+ installationId?: string;
11
+ onError?: (error: Error) => void;
12
+ /** Acknowledges the current value; rejection keeps it queued for retry. */
13
+ onReported?: (userId: string, panelId: string, value: ToolActivityValue) => void | Promise<void>;
14
+ };
15
+ /** Server-only, current-state reporting. No transcript data or browser frame is required. */
16
+ export declare class ToolActivityReporter {
17
+ private readonly options;
18
+ private readonly appId;
19
+ private readonly installation;
20
+ private readonly core;
21
+ private readonly service;
22
+ private readonly entries;
23
+ private readonly controller;
24
+ private readonly timer;
25
+ private pending?;
26
+ private disposed;
27
+ constructor(options?: ToolActivityReporterOptions);
28
+ set(userId: string, panelId: string, value: ToolActivityValue): void;
29
+ /** Serializes changes, retries failures and renews presence. After Core restarts every retained
30
+ * state is re-reported within 20 seconds, without rebinding to another installation. */
31
+ flush(): Promise<void>;
32
+ private sendDue;
33
+ dispose(): Promise<void>;
34
+ }
35
+ export type { ToolActivity };
@@ -0,0 +1,93 @@
1
+ /** Server-only, current-state reporting. No transcript data or browser frame is required. */
2
+ export class ToolActivityReporter {
3
+ options;
4
+ appId;
5
+ installation;
6
+ core;
7
+ service;
8
+ entries = new Map();
9
+ controller = new AbortController();
10
+ timer;
11
+ pending;
12
+ disposed = false;
13
+ constructor(options = {}) {
14
+ this.options = options;
15
+ this.appId = options.appId ?? process.env.HOSTY_APP_ID ?? "";
16
+ this.installation = options.installationId ?? process.env.HOSTY_APP_INSTALLATION_ID ?? "";
17
+ this.core = new URL(options.coreOrigin ?? process.env.HOSTY_CORE_ORIGIN ?? "http://127.0.0.1:7070");
18
+ this.service = options.serviceToken ?? process.env.HOSTY_APP_SERVICE_TOKEN ?? "";
19
+ if (!this.appId || !this.installation || !this.service)
20
+ throw new Error("Tool activity requires a Core-managed installation and app service identity.");
21
+ this.timer = setInterval(() => { void this.flush(); }, 250);
22
+ this.timer.unref?.();
23
+ }
24
+ set(userId, panelId, value) {
25
+ if (this.disposed)
26
+ return;
27
+ if (!userId || !panelId || !["idle", "working", "needs-you", "failed"].includes(value.state)
28
+ || value.count !== undefined && (!Number.isInteger(value.count) || value.count < 0 || value.count > 9999)
29
+ || value.label !== undefined && (value.label.length > 120 || /[\x00-\x1f\x7f]/.test(value.label))
30
+ || value.ttlSeconds !== undefined && (!Number.isInteger(value.ttlSeconds) || value.ttlSeconds < 1 || value.ttlSeconds > 300))
31
+ throw new Error("Invalid tool activity.");
32
+ const key = JSON.stringify([userId, panelId]), previous = this.entries.get(key);
33
+ if (!previous && this.entries.size >= 10_000)
34
+ throw new Error("Tool activity reporter capacity reached.");
35
+ if (previous && JSON.stringify(previous.value) === JSON.stringify(value))
36
+ return;
37
+ this.entries.set(key, { userId, panelId, value: { ...value }, due: 0 });
38
+ void this.flush();
39
+ }
40
+ /** Serializes changes, retries failures and renews presence. After Core restarts every retained
41
+ * state is re-reported within 20 seconds, without rebinding to another installation. */
42
+ flush() {
43
+ if (this.pending)
44
+ return this.pending;
45
+ if (this.disposed)
46
+ return Promise.resolve();
47
+ this.pending = this.sendDue().finally(() => { this.pending = undefined; });
48
+ return this.pending;
49
+ }
50
+ async sendDue() {
51
+ for (const [key, entry] of this.entries) {
52
+ if (this.disposed || entry.due > Date.now())
53
+ continue;
54
+ try {
55
+ const response = await fetch(new URL(`/api/internal/apps/${encodeURIComponent(this.appId)}/ui/activity`, this.core), {
56
+ method: "POST", redirect: "error", cache: "no-store",
57
+ signal: AbortSignal.any([this.controller.signal, AbortSignal.timeout(10_000)]),
58
+ headers: { authorization: `Bearer ${this.service}`, "content-type": "application/json" },
59
+ body: JSON.stringify({ userId: entry.userId, installationId: this.installation, panelId: entry.panelId, ...entry.value }),
60
+ });
61
+ await response.body?.cancel();
62
+ if (!response.ok)
63
+ throw new Error(`Tool activity report failed (${response.status}).`);
64
+ // A newer value may have arrived during this request; never acknowledge or delete it.
65
+ if (this.entries.get(key) !== entry)
66
+ continue;
67
+ await this.options.onReported?.(entry.userId, entry.panelId, { ...entry.value });
68
+ if (this.entries.get(key) !== entry)
69
+ continue;
70
+ if (entry.value.state === "idle")
71
+ this.entries.delete(key);
72
+ else
73
+ entry.due = Date.now() + Math.min(20_000, (entry.value.ttlSeconds ?? 60) * 1000 / 3);
74
+ }
75
+ catch (error) {
76
+ if (this.disposed)
77
+ return;
78
+ entry.due = Date.now() + 5000;
79
+ try {
80
+ this.options.onError?.(error instanceof Error ? error : new Error("Tool activity unavailable."));
81
+ }
82
+ catch { /* Observers cannot stop reporting. */ }
83
+ }
84
+ }
85
+ }
86
+ async dispose() {
87
+ this.disposed = true;
88
+ clearInterval(this.timer);
89
+ this.controller.abort();
90
+ await this.pending;
91
+ this.entries.clear();
92
+ }
93
+ }
@@ -1,6 +1,6 @@
1
1
  import type { SandboxIdentityContext } from "./sandbox-identity.js";
2
2
  import type { ProviderServerOptions } from "@hosty-sdk/app/providers/server";
3
- import { type PanelTarget, type UiBootstrap, type UiPanel, type UiResolvedPanel, type UiNavigationCatalog, type UiNavigationTarget, type UiResolvedNavigation } from "@hosty-sdk/app/ui/types";
3
+ import { type ToolActivity, type PanelTarget, type UiBootstrap, type UiPanel, type UiResolvedPanel, type UiNavigationCatalog, type UiNavigationTarget, type UiResolvedNavigation } from "@hosty-sdk/app/ui/types";
4
4
  /** Server transport for generic UI metadata. Credentials never appear in returned metadata. */
5
5
  export declare class UiServerClient {
6
6
  private readonly appId;
@@ -12,6 +12,7 @@ export declare class UiServerClient {
12
12
  parentOrigin?: string;
13
13
  }, token: string, signal?: AbortSignal): Promise<UiBootstrap>;
14
14
  listPanels(token: string, signal?: AbortSignal): Promise<UiPanel[]>;
15
+ activity(token: string, signal?: AbortSignal): Promise<ToolActivity[]>;
15
16
  navigation(token: string, signal?: AbortSignal): Promise<UiNavigationCatalog>;
16
17
  resolveNavigation(target: UiNavigationTarget, token: string, signal?: AbortSignal): Promise<UiResolvedNavigation>;
17
18
  resolvePanel(target: PanelTarget, token: string, signal?: AbortSignal): Promise<UiResolvedPanel>;
package/dist/ui-server.js CHANGED
@@ -35,6 +35,9 @@ export class UiServerClient {
35
35
  async listPanels(token, signal) {
36
36
  return (await this.request("panels", token, undefined, signal)).panels;
37
37
  }
38
+ async activity(token, signal) {
39
+ return (await this.request("activity", token, undefined, signal)).activities;
40
+ }
38
41
  navigation(token, signal) {
39
42
  return this.request("navigation", token, undefined, signal);
40
43
  }
@@ -70,6 +73,11 @@ export async function createHostyUiResponse(request, context, options = {}) {
70
73
  else if (request.method === "GET" && route === "panels") {
71
74
  result = { panels: await client.listPanels(context.token, request.signal) };
72
75
  }
76
+ else if (request.method === "GET" && route === "activity") {
77
+ if (url.search)
78
+ throw new UiError("ui_request_invalid", "Unexpected activity parameters.", 400);
79
+ result = { activities: await client.activity(context.token, request.signal) };
80
+ }
73
81
  else if (request.method === "GET" && route === "navigation") {
74
82
  if (url.search)
75
83
  throw new UiError("ui_request_invalid", "Unexpected navigation parameters.", 400);
@@ -192,3 +192,13 @@ export type UiRouterAdapter = {
192
192
  subscribe(listener: () => void): () => void;
193
193
  navigate(route: string): void | Promise<void>;
194
194
  };
195
+ /** Activity is owned by Core, independently of mounted tool frames. */
196
+ export type ToolActivityState = "idle" | "working" | "needs-you" | "failed";
197
+ export type ToolActivity = {
198
+ panel: PanelRef;
199
+ state: ToolActivityState | "no-connection";
200
+ count: number | null;
201
+ label: string | null;
202
+ reportedAt: string | null;
203
+ expiresAt: string | null;
204
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hosty-sdk/app",
3
- "version": "0.34.2",
3
+ "version": "0.35.1",
4
4
  "description": "Hosty app auth: session state machine, recovery bridge, Core revalidation, and the embedder responder.",
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",
@@ -128,6 +128,10 @@
128
128
  "./sandbox/react": {
129
129
  "types": "./dist/sandbox-react.d.ts",
130
130
  "default": "./dist/sandbox-react.js"
131
+ },
132
+ "./tool-activity/server": {
133
+ "types": "./dist/tool-activity-server.d.ts",
134
+ "default": "./dist/tool-activity-server.js"
131
135
  }
132
136
  },
133
137
  "scripts": {