@hosty-sdk/app 0.27.22 → 0.27.24

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
@@ -338,6 +338,17 @@ disable local tools; an opening otherwise fails with `presentation_unavailable`.
338
338
  Panel hosting requires reviewed optional `apps.panels.read` (or existing `apps.read`);
339
339
  participating as a child does not require catalog access.
340
340
 
341
+ Panel headers expose **Minimize panel (keep tool open)** (`−`) without a close button.
342
+ Hosts with a tool rail can set `minimizeControl="drawer"` on `HostyPanelWorkspace` to omit the
343
+ minimize button in docked panels and use the active rail icon to toggle them. Responsive drawers
344
+ keep a visible minimize button because the rail is inert while a drawer is open. The default
345
+ `minimizeControl="always"` retains the button in standalone layouts.
346
+ Custom host layouts can use `UiHost.closePresentation(presentationId)` to release a tool view;
347
+ this does not end its conversation or background work.
348
+ To match host chrome, set `--hosty-panel-background`, `--hosty-panel-foreground`,
349
+ `--hosty-panel-muted` and `--hosty-panel-border` on the workspace or an ancestor.
350
+ These overrides apply in both themes; omitted properties retain the SDK light/dark defaults.
351
+
341
352
  ```ts
342
353
  // app/api/hosty/ui/[...action]/route.ts — own app configuration/session only
343
354
  import { createHostyUiRouteHandler } from "@hosty-sdk/app/server";
package/dist/react.js CHANGED
@@ -208,24 +208,34 @@ export function useAppIdentity({ probePath = "/api/auth/identity", appCodePath =
208
208
  const onSessionRef = useRef(onSession);
209
209
  useEffect(() => { onSessionRef.current = onSession; }, [onSession]);
210
210
  const continuity = activity?.continuity;
211
+ const continuityCoreOrigin = activity?.openUrl ? new URL(activity.openUrl).origin : null;
211
212
  const continuityActor = session?.userId;
212
213
  useEffect(() => {
213
- if (!continuity || !activity?.openUrl || !continuityActor)
214
+ if (!continuity || !continuityCoreOrigin || !continuityActor)
214
215
  return;
215
- return configureAppContinuity({ coreOrigin: new URL(activity.openUrl).origin, authority: continuity,
216
+ return configureAppContinuity({ coreOrigin: continuityCoreOrigin, authority: continuity,
216
217
  probe: async () => {
217
218
  const next = await refreshRef.current();
218
219
  return next?.status === "active" && next.userId === continuityActor
219
220
  && (!next.hosty || (next.hosty.version === 1 && next.hosty.setup === "ready"));
220
221
  } });
221
- }, [continuity?.grantId, activity?.openUrl, continuityActor]);
222
+ }, [continuity?.grantId, continuityCoreOrigin, continuityActor]);
222
223
  useEffect(() => {
223
224
  let cancelled = false;
224
225
  let probeSequence = 0;
225
226
  const controller = new AbortController();
226
227
  if (initialGrantPresent.current === null)
227
228
  initialGrantPresent.current = restoreAppGrant();
228
- async function probeAndRecover() {
229
+ let pendingProbe = null;
230
+ // Focus, continuity maintenance and setup review can request the same identity at once.
231
+ // Share its result: a superseded successful probe must not look like an access outage.
232
+ function probeAndRecover() {
233
+ if (pendingProbe)
234
+ return pendingProbe;
235
+ pendingProbe = runProbeAndRecover().finally(() => { pendingProbe = null; });
236
+ return pendingProbe;
237
+ }
238
+ async function runProbeAndRecover() {
229
239
  const sequence = ++probeSequence;
230
240
  // Dedicated controller + setTimeout instead of AbortSignal.any/timeout, which are not
231
241
  // available in every browser (a synchronous throw here would stop the bridge from
@@ -274,7 +284,7 @@ export function useAppIdentity({ probePath = "/api/auth/identity", appCodePath =
274
284
  }
275
285
  if (revision !== appGrantRevision()) {
276
286
  initialGrantPresent.current ||= restoreAppGrant();
277
- return probeAndRecover();
287
+ return runProbeAndRecover();
278
288
  }
279
289
  forgetRejectedAppGrant(rejectionCode, revision);
280
290
  if (openUrl && protocol)
@@ -23,6 +23,8 @@ export type HostyPanelWorkspaceProps = {
23
23
  workspaceId?: string;
24
24
  /** A host-owned tool window can keep dock geometry even in a narrow container. */
25
25
  layout?: "responsive" | "dock";
26
+ /** Keep a visible dismiss control in drawers; hosts with a tool rail can omit it in docks. */
27
+ minimizeControl?: "always" | "drawer";
26
28
  children: ReactNode;
27
29
  railMode?: "opened" | "catalog";
28
30
  className?: string;
@@ -42,4 +44,4 @@ export type HostyPanelWorkspaceProps = {
42
44
  renderIcon?: (panel: UiPanel) => ReactNode;
43
45
  };
44
46
  /** A single retained frame tree for dock and drawer; only CSS placement changes at the breakpoint. */
45
- export declare function HostyPanelWorkspace({ children, workspaceId, layout, railMode, className, initialWidth, initialExpanded, onWidthChange, onExpandedChange, onStartApp, onAskAssistant, onAuthRequired, outbound, renderIcon }: HostyPanelWorkspaceProps): import("react").JSX.Element;
47
+ export declare function HostyPanelWorkspace({ children, workspaceId, layout, minimizeControl, railMode, className, initialWidth, initialExpanded, onWidthChange, onExpandedChange, onStartApp, onAskAssistant, onAuthRequired, outbound, renderIcon }: HostyPanelWorkspaceProps): import("react").JSX.Element;
package/dist/ui-react.js CHANGED
@@ -57,7 +57,7 @@ export function HostyUiRoot({ children, uiPath, panelLayout = "auto", active = t
57
57
  const empty = { bootstrap: null, catalog: [], tools: [], selected: null, expanded: false, width: 360, error: null };
58
58
  const noop = () => () => { };
59
59
  /** A single retained frame tree for dock and drawer; only CSS placement changes at the breakpoint. */
60
- export function HostyPanelWorkspace({ children, workspaceId = DEFAULT_WORKSPACE, layout = "responsive", railMode = "opened", className = "", initialWidth, initialExpanded, onWidthChange, onExpandedChange, onStartApp, onAskAssistant, onAuthRequired, outbound, renderIcon }) {
60
+ export function HostyPanelWorkspace({ children, workspaceId = DEFAULT_WORKSPACE, layout = "responsive", minimizeControl = "always", railMode = "opened", className = "", initialWidth, initialExpanded, onWidthChange, onExpandedChange, onStartApp, onAskAssistant, onAuthRequired, outbound, renderIcon }) {
61
61
  const host = useHostyUiHost();
62
62
  const layer = useContext(UiFrameLayerContext), chrome = useHostyChrome();
63
63
  const viewport = useRef(null);
@@ -65,7 +65,7 @@ export function HostyPanelWorkspace({ children, workspaceId = DEFAULT_WORKSPACE,
65
65
  const unsupported = state.error?.code.startsWith("unsupported") === true;
66
66
  const workspace = state.workspaces?.find(w => w.workspaceId === workspaceId);
67
67
  const workspaceTools = state.tools.filter(t => t.workspaceId === workspaceId);
68
- const root = useRef(null), content = useRef(null), body = useRef(null), close = useRef(null);
68
+ const root = useRef(null), content = useRef(null), body = useRef(null), minimize = useRef(null);
69
69
  const opener = useRef(null), panelOpener = useRef(null);
70
70
  const [size, setSize] = useState(1200), [chooser, setChooser] = useState(false), [error, setError] = useState(null);
71
71
  const chooserRef = useRef(null);
@@ -195,7 +195,7 @@ export function HostyPanelWorkspace({ children, workspaceId = DEFAULT_WORKSPACE,
195
195
  return;
196
196
  window.dispatchEvent(new CustomEvent("hosty:ui:modal", { detail: `tools:${workspaceId}` }));
197
197
  opener.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
198
- close.current?.focus();
198
+ minimize.current?.focus();
199
199
  const dismiss = (event) => { if (event.detail === workspaceId)
200
200
  host?.setExpanded(false, workspaceId); };
201
201
  window.addEventListener("hosty:ui:drawer-dismiss", dismiss);
@@ -279,14 +279,14 @@ export function HostyPanelWorkspace({ children, workspaceId = DEFAULT_WORKSPACE,
279
279
  event.preventDefault();
280
280
  host?.setWidth(width + (event.key === "ArrowLeft" ? 16 : -16), workspaceId);
281
281
  }
282
- } }), _jsxs("header", { children: [_jsx("strong", { children: selected?.label ?? "Tool" }), tool?.resolved.standaloneHref && _jsx("a", { href: tool.resolved.standaloneHref, target: "_blank", rel: "noopener noreferrer", title: "Open separately", "aria-label": "Open separately", children: "\u2197" }), _jsx("button", { ref: close, type: "button", onClick: () => host?.setExpanded(false, workspaceId), "aria-label": "Return to document", title: "Return to document", children: "\u00D7" })] }), tool?.resolved.state === "degraded" && _jsx("div", { className: "hosty-panel-state", role: "status", children: "This app\u2019s health check is failing. Its page may still work." }), tool?.readiness === "unreported" && _jsxs("div", { className: "hosty-panel-state", role: "status", children: ["This tool has not confirmed that it loaded. If the page is blank or refuses to connect, use ", _jsx("a", { href: tool.resolved.standaloneHref ?? undefined, target: "_blank", rel: "noopener noreferrer", children: "Open separately" }), "."] }), tool?.readiness === "auth-required" && _jsx("div", { className: "hosty-panel-state", role: "status", children: "Check access in the tool below, then retry opening the discussion." }), tool && workspaceTools.filter(t => t.key === tool.key).length > 1 && _jsxs("label", { className: "hosty-panel-state", children: ["Open views", _jsx("select", { "aria-label": "Tool instance", value: tool.presentationId, onChange: event => host?.focusPresentation(event.target.value), children: workspaceTools.filter(t => t.key === tool.key).map((item, index) => _jsxs("option", { value: item.presentationId, children: [selected?.label ?? "Tool", " ", index + 1] }, item.presentationId)) })] }), tool && _jsx("button", { type: "button", className: "hosty-panel-state", onClick: () => host?.closePresentation(tool.presentationId), children: "Close this tool view" }), _jsx("div", { ref: viewport, className: "hosty-panel-viewport", "aria-owns": tool ? `hosty-presentation-${tool.presentationId}` : undefined }), drawer && expanded && _jsx("span", { tabIndex: 0, "aria-hidden": "true", className: "hosty-panel-focus-guard", onFocus: () => close.current?.focus() })] }), _jsxs("dialog", { ref: chooserRef, className: "hosty-tool-chooser", onCancel: () => setChooser(false), "aria-label": "Add tool", children: [_jsxs("header", { children: [_jsx("strong", { children: "Add tool" }), _jsx("button", { type: "button", onClick: () => setChooser(false), "aria-label": "Close tool chooser", children: "\u00D7" })] }), _jsx("p", { children: "Open a tool beside your document." }), error && _jsx("p", { role: "status", children: error }), state.error && _jsx("p", { role: "status", children: state.error.message }), !state.bootstrap?.canHost && _jsx("p", { children: "This app needs optional panel access in Hosty Core to show tools here. You can still use the app." }), state.bootstrap?.canHost && !state.catalog.length && _jsx("p", { children: "No tools are available for your account." }), _jsx("ul", { children: state.catalog.map(panel => _jsxs("li", { children: [_jsxs("button", { type: "button", onClick: () => void select(panel), children: [_jsx("strong", { children: panel.label }), _jsxs("span", { children: [panel.appName, panel.available ? "" : ` · ${panel.state}`] })] }), !panel.available && panel.canStart && onStartApp && _jsx("button", { type: "button", onClick: () => void Promise.resolve(onStartApp(panel.panel.appId)).then(() => host?.refresh()), children: "Start app" })] }, panelKey(panel.panel))) }), _jsx("button", { type: "button", onClick: () => void host?.refresh(), children: "Refresh tools" })] }), error && !chooser && _jsxs("div", { className: "hosty-panel-error", role: "status", children: [error, " ", _jsx("button", { type: "button", onClick: () => { setChooser(true); setError(null); }, children: "Choose tool" })] })] });
282
+ } }), _jsxs("header", { children: [_jsx("strong", { children: selected?.label ?? "Tool" }), tool?.resolved.standaloneHref && _jsx("a", { href: tool.resolved.standaloneHref, target: "_blank", rel: "noopener noreferrer", title: "Open separately", "aria-label": "Open separately", children: "\u2197" }), (drawer || minimizeControl === "always") && _jsx("button", { ref: minimize, type: "button", onClick: () => host?.setExpanded(false, workspaceId), "aria-label": "Minimize panel", title: "Minimize panel (keep tool open)", children: _jsx("span", { "aria-hidden": "true", children: "\u2212" }) })] }), tool?.resolved.state === "degraded" && _jsx("div", { className: "hosty-panel-state", role: "status", children: "This app\u2019s health check is failing. Its page may still work." }), tool?.readiness === "unreported" && _jsxs("div", { className: "hosty-panel-state", role: "status", children: ["This tool has not confirmed that it loaded. If the page is blank or refuses to connect, use ", _jsx("a", { href: tool.resolved.standaloneHref ?? undefined, target: "_blank", rel: "noopener noreferrer", children: "Open separately" }), "."] }), tool?.readiness === "auth-required" && _jsx("div", { className: "hosty-panel-state", role: "status", children: "Check access in the tool below, then retry opening the discussion." }), tool && workspaceTools.filter(t => t.key === tool.key).length > 1 && _jsxs("label", { className: "hosty-panel-state", children: ["Open views", _jsx("select", { "aria-label": "Tool instance", value: tool.presentationId, onChange: event => host?.focusPresentation(event.target.value), children: workspaceTools.filter(t => t.key === tool.key).map((item, index) => _jsxs("option", { value: item.presentationId, children: [selected?.label ?? "Tool", " ", index + 1] }, item.presentationId)) })] }), _jsx("div", { ref: viewport, className: "hosty-panel-viewport", "aria-owns": tool ? `hosty-presentation-${tool.presentationId}` : undefined }), drawer && expanded && _jsx("span", { tabIndex: 0, "aria-hidden": "true", className: "hosty-panel-focus-guard", onFocus: () => minimize.current?.focus() })] }), _jsxs("dialog", { ref: chooserRef, className: "hosty-tool-chooser", onCancel: () => setChooser(false), "aria-label": "Add tool", children: [_jsxs("header", { children: [_jsx("strong", { children: "Add tool" }), _jsx("button", { type: "button", onClick: () => setChooser(false), "aria-label": "Close tool chooser", children: "\u00D7" })] }), _jsx("p", { children: "Open a tool beside your document." }), error && _jsx("p", { role: "status", children: error }), state.error && _jsx("p", { role: "status", children: state.error.message }), !state.bootstrap?.canHost && _jsx("p", { children: "This app needs optional panel access in Hosty Core to show tools here. You can still use the app." }), state.bootstrap?.canHost && !state.catalog.length && _jsx("p", { children: "No tools are available for your account." }), _jsx("ul", { children: state.catalog.map(panel => _jsxs("li", { children: [_jsxs("button", { type: "button", onClick: () => void select(panel), children: [_jsx("strong", { children: panel.label }), _jsxs("span", { children: [panel.appName, panel.available ? "" : ` · ${panel.state}`] })] }), !panel.available && panel.canStart && onStartApp && _jsx("button", { type: "button", onClick: () => void Promise.resolve(onStartApp(panel.panel.appId)).then(() => host?.refresh()), children: "Start app" })] }, panelKey(panel.panel))) }), _jsx("button", { type: "button", onClick: () => void host?.refresh(), children: "Refresh tools" })] }), error && !chooser && _jsxs("div", { className: "hosty-panel-error", role: "status", children: [error, " ", _jsx("button", { type: "button", onClick: () => { setChooser(true); setError(null); }, children: "Choose tool" })] })] });
283
283
  }
284
284
  const styles = `
285
285
  .hosty-panel-viewport{flex:1;min-height:0;min-width:0}
286
286
  .hosty-app-layout{display:flex;width:100%;height:100%;min-width:0;min-height:0}.hosty-app-content{flex:1;min-width:0;min-height:0;overflow:auto}
287
287
 
288
- .hosty-panel-workspace{--hp-bg:#f8f9fb;--hp-fg:#20242c;--hp-muted:#606875;--hp-border:#e0e4eb;display:flex;position:relative;min-width:0;min-height:0;width:100%;height:100dvh;overflow:hidden;background:var(--hp-bg);color:var(--hp-fg)}
289
- html.dark .hosty-panel-workspace,html[data-hosty-theme=dark] .hosty-panel-workspace{--hp-bg:#14171c;--hp-fg:#f0f2f5;--hp-muted:#a2aab7;--hp-border:#323843}
288
+ .hosty-panel-workspace{--hp-bg:var(--hosty-panel-background,#f8f9fb);--hp-fg:var(--hosty-panel-foreground,#20242c);--hp-muted:var(--hosty-panel-muted,#606875);--hp-border:var(--hosty-panel-border,#e0e4eb);display:flex;position:relative;min-width:0;min-height:0;width:100%;height:100dvh;overflow:hidden;background:var(--hp-bg);color:var(--hp-fg)}
289
+ html.dark .hosty-panel-workspace,html[data-hosty-theme=dark] .hosty-panel-workspace{--hp-bg:var(--hosty-panel-background,#14171c);--hp-fg:var(--hosty-panel-foreground,#f0f2f5);--hp-muted:var(--hosty-panel-muted,#a2aab7);--hp-border:var(--hosty-panel-border,#323843)}
290
290
  .hosty-panel-document{flex:1;min-width:0;min-height:0;overflow:auto}.hosty-panel-rail{order:3;flex:0 0 48px;display:flex;flex-direction:column;align-items:center;gap:8px;padding:10px 0;border-left:1px solid var(--hp-border);overflow:auto}
291
291
  .hosty-panel-workspace button,.hosty-panel-workspace a{font:inherit}.hosty-panel-rail button,.hosty-panel-body header button,.hosty-panel-body header a,.hosty-tool-chooser header button{position:relative;display:grid;place-items:center;flex-shrink:0;border:1px solid transparent;border-radius:8px;width:32px;height:32px;padding:0;background:transparent;color:inherit;font:500 13px system-ui;cursor:pointer;text-decoration:none}
292
292
  .hosty-panel-rail button:hover,.hosty-panel-rail button[aria-expanded=true]{border-color:var(--hp-border);background:color-mix(in srgb,var(--hp-fg) 8%,transparent)}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hosty-sdk/app",
3
- "version": "0.27.22",
3
+ "version": "0.27.24",
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",