@agent-native/dispatch 0.25.1 → 0.27.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.
Files changed (30) hide show
  1. package/dist/actions/provider-api-register.d.ts +14 -14
  2. package/dist/components/create-app-popover.d.ts +7 -2
  3. package/dist/components/create-app-popover.d.ts.map +1 -1
  4. package/dist/components/create-app-popover.js +4 -3
  5. package/dist/components/create-app-popover.js.map +1 -1
  6. package/dist/components/index.d.ts +1 -0
  7. package/dist/components/index.d.ts.map +1 -1
  8. package/dist/components/index.js +1 -0
  9. package/dist/components/index.js.map +1 -1
  10. package/dist/components/layout/Layout.d.ts.map +1 -1
  11. package/dist/components/layout/Layout.js +42 -2
  12. package/dist/components/layout/Layout.js.map +1 -1
  13. package/dist/components/simple-agents-panel.d.ts +4 -1
  14. package/dist/components/simple-agents-panel.d.ts.map +1 -1
  15. package/dist/components/simple-agents-panel.js +46 -13
  16. package/dist/components/simple-agents-panel.js.map +1 -1
  17. package/dist/components/workspace-app-host.d.ts +3 -1
  18. package/dist/components/workspace-app-host.d.ts.map +1 -1
  19. package/dist/components/workspace-app-host.js +87 -5
  20. package/dist/components/workspace-app-host.js.map +1 -1
  21. package/package.json +3 -3
  22. package/src/components/create-app-popover.spec.tsx +9 -1
  23. package/src/components/create-app-popover.tsx +9 -1
  24. package/src/components/index.ts +1 -0
  25. package/src/components/layout/Layout.app-chat.spec.ts +22 -0
  26. package/src/components/layout/Layout.tsx +72 -3
  27. package/src/components/simple-agents-panel.spec.tsx +81 -1
  28. package/src/components/simple-agents-panel.tsx +156 -71
  29. package/src/components/workspace-app-host.spec.tsx +57 -0
  30. package/src/components/workspace-app-host.tsx +139 -3
@@ -1,3 +1,4 @@
1
+ import { AgentSidebar } from "@agent-native/core/client/agent-chat";
1
2
  import {
2
3
  ChatFirstAppPane,
3
4
  defaultChatFirstCopy,
@@ -10,8 +11,14 @@ import {
10
11
  } from "@agent-native/core/client/hooks";
11
12
  import { useT } from "@agent-native/core/client/i18n";
12
13
  import { withBuilderUtmTrackingParams } from "@agent-native/core/shared/builder-link-tracking";
13
- import { IconArrowLeft, IconClockHour4 } from "@tabler/icons-react";
14
- import { useEffect, useMemo, useRef, useState } from "react";
14
+ import {
15
+ IconArrowLeft,
16
+ IconArrowUpRight,
17
+ IconClockHour4,
18
+ IconLock,
19
+ } from "@tabler/icons-react";
20
+ import { useTheme } from "next-themes";
21
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
15
22
  import { Link } from "react-router";
16
23
 
17
24
  import { isEmbedSessionExpiredMessage } from "../lib/embed-session-recovery";
@@ -38,6 +45,40 @@ interface EmbedSessionInput {
38
45
  chrome: "minimal";
39
46
  }
40
47
 
48
+ type WorkspaceAppTheme = "light" | "dark";
49
+ type WorkspaceAppAuthState = "unknown" | "authenticated" | "unauthenticated";
50
+
51
+ function resolveWorkspaceAppAuthState(
52
+ rawUrl: string | null,
53
+ ): WorkspaceAppAuthState {
54
+ if (!rawUrl) return "unknown";
55
+ try {
56
+ const lastSegment = new URL(rawUrl, window.location.origin).pathname
57
+ .split("/")
58
+ .filter(Boolean)
59
+ .at(-1)
60
+ ?.toLowerCase();
61
+ if (
62
+ lastSegment === "sign-in" ||
63
+ lastSegment === "login" ||
64
+ lastSegment === "signup"
65
+ ) {
66
+ return "unauthenticated";
67
+ }
68
+ return "authenticated";
69
+ } catch {
70
+ return "unknown";
71
+ }
72
+ }
73
+
74
+ function buildWorkspaceAppThemeUpdate(theme: WorkspaceAppTheme) {
75
+ return {
76
+ type: "agent-native-theme-update" as const,
77
+ theme,
78
+ isDark: theme === "dark",
79
+ };
80
+ }
81
+
41
82
  export function buildChatFirstEmbedSessionInput(
42
83
  appId: string,
43
84
  path: string,
@@ -56,18 +97,49 @@ interface WorkspaceAppFrameProps {
56
97
  app: WorkspaceAppFrameApp;
57
98
  /** Chat-first app tabs use their own route while standalone hosts use app metadata. */
58
99
  embedPath?: string;
100
+ /** Chat-first app surfaces own the parent chat rail around the iframe. */
101
+ chatSidebar?: boolean;
59
102
  copy?: ChatFirstCopy;
60
103
  }
61
104
 
62
105
  export function WorkspaceAppFrame({
63
106
  app,
64
107
  embedPath,
108
+ chatSidebar = false,
65
109
  copy = defaultChatFirstCopy,
66
110
  }: WorkspaceAppFrameProps) {
111
+ const { resolvedTheme } = useTheme();
112
+ const theme: WorkspaceAppTheme =
113
+ resolvedTheme === "dark" || resolvedTheme === "light"
114
+ ? resolvedTheme
115
+ : typeof document !== "undefined" &&
116
+ document.documentElement.classList.contains("dark")
117
+ ? "dark"
118
+ : "light";
67
119
  const [embedUrl, setEmbedUrl] = useState<string | null>(null);
68
120
  const [embedError, setEmbedError] = useState<Error | null>(null);
69
121
  const [embedAttempt, setEmbedAttempt] = useState(0);
122
+ const [authState, setAuthState] = useState<WorkspaceAppAuthState>("unknown");
70
123
  const embedFrameRef = useRef<HTMLIFrameElement>(null);
124
+ const postThemeToFrame = useCallback(() => {
125
+ embedFrameRef.current?.contentWindow?.postMessage(
126
+ buildWorkspaceAppThemeUpdate(theme),
127
+ "*",
128
+ );
129
+ }, [theme]);
130
+ const handleFrameLoad = useCallback(() => {
131
+ postThemeToFrame();
132
+ const frame = embedFrameRef.current;
133
+ let frameUrl = embedUrl;
134
+ try {
135
+ frameUrl = frame?.contentWindow?.location.href ?? frameUrl;
136
+ // coercion-ok: Cross-origin frames cannot expose location; their auth state arrives by message.
137
+ } catch {
138
+ // Cross-origin app frames report their auth state through postMessage.
139
+ }
140
+ const nextAuthState = resolveWorkspaceAppAuthState(frameUrl);
141
+ if (nextAuthState !== "unknown") setAuthState(nextAuthState);
142
+ }, [embedUrl, postThemeToFrame]);
71
143
  const workspaceSsoEnabled = useFeatureFlag(DISPATCH_WORKSPACE_SSO_FLAG.key);
72
144
  const createEmbedSession = useActionMutation<
73
145
  EmbedSessionResult,
@@ -104,6 +176,7 @@ export function WorkspaceAppFrame({
104
176
  let cancelled = false;
105
177
  setEmbedUrl(null);
106
178
  setEmbedError(null);
179
+ setAuthState("unknown");
107
180
  const createSession = workspaceSsoEnabled
108
181
  ? createWorkspaceSsoEmbedSession
109
182
  : createEmbedSession;
@@ -154,7 +227,26 @@ export function WorkspaceAppFrame({
154
227
  window.removeEventListener("message", handleEmbedSessionExpired);
155
228
  }, [embedUrl]);
156
229
 
157
- return (
230
+ useEffect(() => {
231
+ const handleAuthState = (event: MessageEvent) => {
232
+ const frame = embedFrameRef.current;
233
+ if (!frame || event.source !== frame.contentWindow) return;
234
+ if (event.data?.type !== "agentNative.authState") return;
235
+ const status = event.data.data?.status;
236
+ if (status === "authenticated" || status === "unauthenticated") {
237
+ setAuthState(status);
238
+ }
239
+ };
240
+
241
+ window.addEventListener("message", handleAuthState);
242
+ return () => window.removeEventListener("message", handleAuthState);
243
+ }, []);
244
+
245
+ useEffect(() => {
246
+ postThemeToFrame();
247
+ }, [embedUrl, postThemeToFrame]);
248
+
249
+ const appPane = (
158
250
  <ChatFirstAppPane
159
251
  app={app}
160
252
  status={
@@ -178,6 +270,7 @@ export function WorkspaceAppFrame({
178
270
  src={url}
179
271
  title={title ?? app.name}
180
272
  ref={embedFrameRef}
273
+ onLoad={handleFrameLoad}
181
274
  referrerPolicy="no-referrer"
182
275
  allow="clipboard-read; clipboard-write"
183
276
  className="h-full w-full border-0 bg-background"
@@ -186,6 +279,49 @@ export function WorkspaceAppFrame({
186
279
  copy={copy}
187
280
  />
188
281
  );
282
+
283
+ if (!chatSidebar) return appPane;
284
+
285
+ return (
286
+ <AgentSidebar
287
+ position="left"
288
+ defaultOpen
289
+ openStorageKey="dispatch-app-chat"
290
+ storageKey={`dispatch-app-chat:${app.id}`}
291
+ scope={{
292
+ type: "workspace-app",
293
+ id: app.id,
294
+ label: app.name,
295
+ contextKey: `workspace-app:${app.id}`,
296
+ }}
297
+ agentChatSurface="app"
298
+ showTabBar
299
+ suppressInlineOpenApp
300
+ dynamicSuggestions={false}
301
+ suggestions={[]}
302
+ emptyStateText={`Ask about ${app.name}`}
303
+ composerSlot={
304
+ authState === "unauthenticated" ? (
305
+ <div className="flex shrink-0 items-center px-3 pb-1">
306
+ <button
307
+ type="button"
308
+ data-dispatch-app-sign-in
309
+ aria-label={`Sign in to ${app.name} on the right`}
310
+ title={`Sign in to ${app.name} on the right`}
311
+ onClick={() => embedFrameRef.current?.focus()}
312
+ className="inline-flex h-6 shrink-0 items-center gap-1 rounded-full border border-border/70 bg-background/60 px-2 text-[11px] font-medium text-muted-foreground transition-colors hover:bg-accent hover:text-foreground"
313
+ >
314
+ <IconLock size={12} stroke={1.8} />
315
+ <span>Sign in on the right</span>
316
+ <IconArrowUpRight size={12} stroke={1.8} />
317
+ </button>
318
+ </div>
319
+ ) : null
320
+ }
321
+ >
322
+ {appPane}
323
+ </AgentSidebar>
324
+ );
189
325
  }
190
326
 
191
327
  export function WorkspaceAppHost({ appId }: { appId?: string }) {